mirror of
https://github.com/Combodo/iTop.git
synced 2026-03-16 22:44:13 +01:00
Compare commits
108 Commits
saas-1.0.1
...
issue/6667
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4af0a027 | ||
|
|
3366bae0ab | ||
|
|
03b484c349 | ||
|
|
70081ecf33 | ||
|
|
575ba1cd7b | ||
|
|
d130959692 | ||
|
|
a8c689c6c0 | ||
|
|
1990ccb5d8 | ||
|
|
e107be56e4 | ||
|
|
0f8e87e001 | ||
|
|
d92d2b5e9e | ||
|
|
ebd0136773 | ||
|
|
f6653e1594 | ||
|
|
65bb76b9e3 | ||
|
|
f238593966 | ||
|
|
d951d3b872 | ||
|
|
ccceb870e3 | ||
|
|
ed6df77cbb | ||
|
|
1ad28312ec | ||
|
|
f002aa04cd | ||
|
|
b86d70623e | ||
|
|
fe3467309d | ||
|
|
851ab9c356 | ||
|
|
aef3c2e609 | ||
|
|
5212e15cc4 | ||
|
|
f04fc546b5 | ||
|
|
caf3076b12 | ||
|
|
c4c400d852 | ||
|
|
6cc4cc4fb6 | ||
|
|
d7495af207 | ||
|
|
13ad98b9b3 | ||
|
|
4be54fdd65 | ||
|
|
6d13397ba1 | ||
|
|
48e7e0309a | ||
|
|
2ce9b2afaf | ||
|
|
d64a91d4ce | ||
|
|
c0c8a13864 | ||
|
|
5ffa41bc16 | ||
|
|
d2eef06276 | ||
|
|
77b14c516e | ||
|
|
880a824f2f | ||
|
|
f7f1b5f399 | ||
|
|
18efbfa803 | ||
|
|
7aa478d6ff | ||
|
|
97700dbf15 | ||
|
|
c25c69d746 | ||
|
|
734a788340 | ||
|
|
eb1eb15791 | ||
|
|
a84077782d | ||
|
|
26048150d3 | ||
|
|
e5b6e2eb8c | ||
|
|
87b6ea4def | ||
|
|
72873a3343 | ||
|
|
5ef25ccb77 | ||
|
|
1682a85cc0 | ||
|
|
cd9beec313 | ||
|
|
8295eaed90 | ||
|
|
86a7cefa68 | ||
|
|
829b648dd2 | ||
|
|
5475b9fbbe | ||
|
|
6f8e7c7002 | ||
|
|
67ca554261 | ||
|
|
f89953f39e | ||
|
|
772368ef8a | ||
|
|
2e049aa244 | ||
|
|
a57b6471c9 | ||
|
|
bc7c1b4744 | ||
|
|
12c78697f4 | ||
|
|
046e857768 | ||
|
|
4d8246c4d8 | ||
|
|
5c61d725e1 | ||
|
|
0c7195f1a3 | ||
|
|
00b070b3cf | ||
|
|
2c4cad4dac | ||
|
|
9c37d5c23e | ||
|
|
89145593ef | ||
|
|
b2e80d37dd | ||
|
|
6432678de9 | ||
|
|
952194b385 | ||
|
|
bfb452dd69 | ||
|
|
64baeba1c7 | ||
|
|
71ed784c60 | ||
|
|
da45651121 | ||
|
|
d388ce9a06 | ||
|
|
47e71d8838 | ||
|
|
2b5973ec67 | ||
|
|
e58918f53e | ||
|
|
125715af3f | ||
|
|
ea8e7c5131 | ||
|
|
06e5e0b102 | ||
|
|
5247f5b3ea | ||
|
|
78396d8e4a | ||
|
|
f1ee22cbed | ||
|
|
39305468f8 | ||
|
|
32fd75bc4b | ||
|
|
efadf2cc79 | ||
|
|
40d63a2fa4 | ||
|
|
baa6dedbcf | ||
|
|
556b9ad89a | ||
|
|
9afc22bd8f | ||
|
|
ef0b0f88c9 | ||
|
|
a010239efb | ||
|
|
264a8cd70a | ||
|
|
aa1834170b | ||
|
|
f94d67ab35 | ||
|
|
3048c8c41f | ||
|
|
246e4a9f50 | ||
|
|
0001e8ffc4 |
@@ -62,6 +62,12 @@ gitGraph
|
||||
commit id: "2022-12-28" tag: "2.7.8"
|
||||
checkout support/3.0
|
||||
commit id: "2023-04-12" tag: "3.0.3"
|
||||
checkout develop
|
||||
commit id: "2023-06-19" tag: "3.1.0-beta" type: REVERSE
|
||||
commit id: "2023-07-26" tag: "3.1.0-1" type: HIGHLIGHT
|
||||
branch support/3.1 order: 840
|
||||
checkout support/3.1
|
||||
commit id: "2023-08-09" tag: "3.1.0-2"
|
||||
```
|
||||
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
|
||||
@@ -228,7 +228,7 @@ class URP_UserProfile extends UserRightsBaseClassGUI
|
||||
"db_table" => "priv_urp_userprofile",
|
||||
"db_key_field" => "id",
|
||||
"db_finalclass_field" => "",
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 N°5324 */
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 */
|
||||
'uniqueness_rules' => array(
|
||||
'no_duplicate' => array(
|
||||
'attributes' => array(
|
||||
|
||||
@@ -23,7 +23,7 @@ define('PORTAL_PROFILE_NAME', 'Portal user');
|
||||
class UserRightsBaseClassGUI extends cmdbAbstractObject
|
||||
{
|
||||
// Whenever something changes, reload the privileges
|
||||
|
||||
|
||||
protected function AfterInsert()
|
||||
{
|
||||
UserRights::FlushPrivileges();
|
||||
@@ -43,7 +43,7 @@ class UserRightsBaseClassGUI extends cmdbAbstractObject
|
||||
class UserRightsBaseClass extends DBObject
|
||||
{
|
||||
// Whenever something changes, reload the privileges
|
||||
|
||||
|
||||
protected function AfterInsert()
|
||||
{
|
||||
UserRights::FlushPrivileges();
|
||||
@@ -100,7 +100,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
$this->m_bCheckReservedNames = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static $m_aActions = array(
|
||||
UR_ACTION_READ => 'Read',
|
||||
UR_ACTION_MODIFY => 'Modify',
|
||||
@@ -113,7 +113,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
protected static $m_aCacheActionGrants = null;
|
||||
protected static $m_aCacheStimulusGrants = null;
|
||||
protected static $m_aCacheProfiles = null;
|
||||
|
||||
|
||||
public static function DoCreateProfile($sName, $sDescription, $bReservedName = false)
|
||||
{
|
||||
if (is_null(self::$m_aCacheProfiles))
|
||||
@@ -125,7 +125,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
{
|
||||
self::$m_aCacheProfiles[$oProfile->Get('name')] = $oProfile->GetKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sCacheKey = $sName;
|
||||
if (isset(self::$m_aCacheProfiles[$sCacheKey]))
|
||||
@@ -137,17 +137,17 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
$oNewObj->Set('description', $sDescription);
|
||||
if ($bReservedName)
|
||||
{
|
||||
$oNewObj->DisableCheckOnReservedNames();
|
||||
$oNewObj->DisableCheckOnReservedNames();
|
||||
}
|
||||
$iId = $oNewObj->DBInsertNoReload();
|
||||
self::$m_aCacheProfiles[$sCacheKey] = $iId;
|
||||
self::$m_aCacheProfiles[$sCacheKey] = $iId;
|
||||
return $iId;
|
||||
}
|
||||
|
||||
|
||||
public static function DoCreateActionGrant($iProfile, $iAction, $sClass, $bPermission = true)
|
||||
{
|
||||
$sAction = self::$m_aActions[$iAction];
|
||||
|
||||
|
||||
if (is_null(self::$m_aCacheActionGrants))
|
||||
{
|
||||
self::$m_aCacheActionGrants = array();
|
||||
@@ -157,7 +157,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
{
|
||||
self::$m_aCacheActionGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('action').'-'.$oGrant->Get('class')] = $oGrant->GetKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sCacheKey = "$iProfile-$sAction-$sClass";
|
||||
if (isset(self::$m_aCacheActionGrants[$sCacheKey]))
|
||||
@@ -171,10 +171,10 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
$oNewObj->Set('class', $sClass);
|
||||
$oNewObj->Set('action', $sAction);
|
||||
$iId = $oNewObj->DBInsertNoReload();
|
||||
self::$m_aCacheActionGrants[$sCacheKey] = $iId;
|
||||
self::$m_aCacheActionGrants[$sCacheKey] = $iId;
|
||||
return $iId;
|
||||
}
|
||||
|
||||
|
||||
public static function DoCreateStimulusGrant($iProfile, $sStimulusCode, $sClass)
|
||||
{
|
||||
if (is_null(self::$m_aCacheStimulusGrants))
|
||||
@@ -186,7 +186,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
{
|
||||
self::$m_aCacheStimulusGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('stimulus').'-'.$oGrant->Get('class')] = $oGrant->GetKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sCacheKey = "$iProfile-$sStimulusCode-$sClass";
|
||||
if (isset(self::$m_aCacheStimulusGrants[$sCacheKey]))
|
||||
@@ -199,13 +199,13 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
$oNewObj->Set('class', $sClass);
|
||||
$oNewObj->Set('stimulus', $sStimulusCode);
|
||||
$iId = $oNewObj->DBInsertNoReload();
|
||||
self::$m_aCacheStimulusGrants[$sCacheKey] = $iId;
|
||||
self::$m_aCacheStimulusGrants[$sCacheKey] = $iId;
|
||||
return $iId;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Create the built-in Administrator profile with its reserved name
|
||||
*/
|
||||
*/
|
||||
public static function DoCreateAdminProfile()
|
||||
{
|
||||
self::DoCreateProfile(ADMIN_PROFILE_NAME, 'Has the rights on everything (bypassing any control)', true /* reserved name */);
|
||||
@@ -213,7 +213,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
|
||||
/*
|
||||
* Overload the standard behavior to preserve reserved names
|
||||
*/
|
||||
*/
|
||||
public function DoCheckToWrite()
|
||||
{
|
||||
parent::DoCheckToWrite();
|
||||
@@ -255,7 +255,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
return '<span style="background-color: #ffdddd;">'.Dict::S('UI:UserManagement:ActionAllowed:No').'</span>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function DoShowGrantSumary($oPage)
|
||||
{
|
||||
if ($this->GetRawName() == "Administrator")
|
||||
@@ -267,7 +267,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
|
||||
// Note: for sure, we assume that the instance is derived from UserRightsProfile
|
||||
$oUserRights = UserRights::GetModuleInstance();
|
||||
|
||||
|
||||
$aDisplayData = array();
|
||||
foreach (MetaModel::GetClasses('bizmodel') as $sClass)
|
||||
{
|
||||
@@ -284,7 +284,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
}
|
||||
}
|
||||
$sStimuli = implode(', ', $aStimuli);
|
||||
|
||||
|
||||
$aDisplayData[] = array(
|
||||
'class' => MetaModel::GetName($sClass),
|
||||
'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'),
|
||||
@@ -296,7 +296,7 @@ class URP_Profiles extends UserRightsBaseClassGUI
|
||||
'stimuli' => $sStimuli,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$aDisplayConfig = array();
|
||||
$aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+'));
|
||||
$aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+'));
|
||||
@@ -334,7 +334,7 @@ class URP_UserProfile extends UserRightsBaseClassGUI
|
||||
"db_table" => "priv_urp_userprofile",
|
||||
"db_key_field" => "id",
|
||||
"db_finalclass_field" => "",
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 N°5324 */
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 */
|
||||
);
|
||||
MetaModel::Init_Params($aParams);
|
||||
//MetaModel::Init_InheritAttributes();
|
||||
@@ -611,7 +611,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
$oSearch->AllowAllData();
|
||||
$oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid'));
|
||||
$oSearch->AddConditionExpression($oCondition);
|
||||
|
||||
|
||||
$oUserOrgSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser));
|
||||
while ($oUserOrg = $oUserOrgSet->Fetch())
|
||||
{
|
||||
@@ -633,7 +633,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
$oSearch->AllowAllData();
|
||||
$oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid'));
|
||||
$oSearch->AddConditionExpression($oCondition);
|
||||
|
||||
|
||||
$this->m_aUserProfiles[$iUser] = array();
|
||||
$oUserProfileSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser));
|
||||
while ($oUserProfile = $oUserProfileSet->Fetch())
|
||||
@@ -648,7 +648,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
public function ResetCache()
|
||||
{
|
||||
// Loaded by Load cache
|
||||
$this->m_aProfiles = null;
|
||||
$this->m_aProfiles = null;
|
||||
$this->m_aUserProfiles = array();
|
||||
$this->m_aUserOrgs = array();
|
||||
|
||||
@@ -658,7 +658,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
// Loaded on demand (time consuming as compared to the others)
|
||||
$this->m_aClassActionGrants = null;
|
||||
$this->m_aClassStimulusGrants = null;
|
||||
|
||||
|
||||
$this->m_aObjectActionGrants = array();
|
||||
}
|
||||
|
||||
@@ -694,10 +694,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
}
|
||||
|
||||
$oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles"));
|
||||
$this->m_aProfiles = array();
|
||||
$this->m_aProfiles = array();
|
||||
while ($oProfile = $oProfileSet->Fetch())
|
||||
{
|
||||
$this->m_aProfiles[$oProfile->GetKey()] = $oProfile;
|
||||
$this->m_aProfiles[$oProfile->GetKey()] = $oProfile;
|
||||
}
|
||||
|
||||
$this->m_aClassStimulusGrants = array();
|
||||
@@ -871,7 +871,7 @@ exit;
|
||||
$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes;
|
||||
return $aRes;
|
||||
}
|
||||
|
||||
|
||||
public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null)
|
||||
{
|
||||
$this->LoadCache();
|
||||
@@ -1009,8 +1009,8 @@ exit;
|
||||
|
||||
/**
|
||||
* Find out which attribute is corresponding the the dimension 'owner org'
|
||||
* returns null if no such attribute has been found (no filtering should occur)
|
||||
*/
|
||||
* returns null if no such attribute has been found (no filtering should occur)
|
||||
*/
|
||||
public static function GetOwnerOrganizationAttCode($sClass)
|
||||
{
|
||||
$sAttCode = null;
|
||||
|
||||
@@ -22,9 +22,9 @@ define('ADMIN_PROFILE_ID', 1);
|
||||
class UserRightsBaseClass extends cmdbAbstractObject
|
||||
{
|
||||
// Whenever something changes, reload the privileges
|
||||
|
||||
|
||||
// Whenever something changes, reload the privileges
|
||||
|
||||
|
||||
protected function AfterInsert()
|
||||
{
|
||||
UserRights::FlushPrivileges();
|
||||
@@ -78,7 +78,7 @@ class URP_Profiles extends UserRightsBaseClass
|
||||
function GetGrantAsHtml($oUserRights, $sClass, $sAction)
|
||||
{
|
||||
$oGrant = $oUserRights->GetClassActionGrant($this->GetKey(), $sClass, $sAction);
|
||||
if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes'))
|
||||
if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes'))
|
||||
{
|
||||
return '<span style="background-color: #ddffdd;">'.Dict::S('UI:UserManagement:ActionAllowed:Yes').'</span>';
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class URP_Profiles extends UserRightsBaseClass
|
||||
return '<span style="background-color: #ffdddd;">'.Dict::S('UI:UserManagement:ActionAllowed:No').'</span>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function DoShowGrantSumary($oPage)
|
||||
{
|
||||
if ($this->GetRawName() == "Administrator")
|
||||
@@ -99,7 +99,7 @@ class URP_Profiles extends UserRightsBaseClass
|
||||
|
||||
// Note: for sure, we assume that the instance is derived from UserRightsProjection
|
||||
$oUserRights = UserRights::GetModuleInstance();
|
||||
|
||||
|
||||
$aDisplayData = array();
|
||||
foreach (MetaModel::GetClasses('bizmodel') as $sClass)
|
||||
{
|
||||
@@ -116,7 +116,7 @@ class URP_Profiles extends UserRightsBaseClass
|
||||
}
|
||||
}
|
||||
$sStimuli = implode(', ', $aStimuli);
|
||||
|
||||
|
||||
$aDisplayData[] = array(
|
||||
'class' => MetaModel::GetName($sClass),
|
||||
'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'),
|
||||
@@ -128,7 +128,7 @@ class URP_Profiles extends UserRightsBaseClass
|
||||
'stimuli' => $sStimuli,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$aDisplayConfig = array();
|
||||
$aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+'));
|
||||
$aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+'));
|
||||
@@ -277,7 +277,7 @@ class URP_UserProfile extends UserRightsBaseClass
|
||||
"db_table" => "priv_urp_userprofile",
|
||||
"db_key_field" => "id",
|
||||
"db_finalclass_field" => "",
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 N°5324 */
|
||||
"is_link" => true, /** @since 3.1.0 N°6482 */
|
||||
);
|
||||
MetaModel::Init_Params($aParams);
|
||||
//MetaModel::Init_InheritAttributes();
|
||||
@@ -356,7 +356,7 @@ class URP_ProfileProjection extends UserRightsBaseClass
|
||||
{
|
||||
$aRes = array($oUser->Get($sColumn));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
elseif (($sExpr == '<any>') || ($sExpr == ''))
|
||||
{
|
||||
@@ -427,14 +427,14 @@ class URP_ClassProjection extends UserRightsBaseClass
|
||||
{
|
||||
$aRes = array($oObject->Get($sColumn));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
elseif (($sExpr == '<any>') || ($sExpr == ''))
|
||||
{
|
||||
$aRes = null;
|
||||
}
|
||||
elseif (strtolower(substr($sExpr, 0, 6)) == 'select')
|
||||
{
|
||||
{
|
||||
$sColumn = $this->Get('attribute');
|
||||
// SELECT...
|
||||
$oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/);
|
||||
@@ -585,14 +585,14 @@ class UserRightsProjection extends UserRightsAddOnAPI
|
||||
$oContact->Set('org_id', $iOrgId);
|
||||
$oContact->Set('email', 'my.email@foo.org');
|
||||
$iContactId = $oContact->DBInsertNoReload();
|
||||
|
||||
|
||||
$oUser = new UserLocal();
|
||||
$oUser->Set('login', $sAdminUser);
|
||||
$oUser->Set('password', $sAdminPwd);
|
||||
$oUser->Set('contactid', $iContactId);
|
||||
$oUser->Set('language', $sLanguage); // Language was chosen during the installation
|
||||
$iUserId = $oUser->DBInsertNoReload();
|
||||
|
||||
|
||||
// Add this user to the very specific 'admin' profile
|
||||
$oUserProfile = new URP_UserProfile();
|
||||
$oUserProfile->Set('userid', $iUserId);
|
||||
@@ -643,24 +643,24 @@ class UserRightsProjection extends UserRightsAddOnAPI
|
||||
// Could be loaded in a shared memory (?)
|
||||
|
||||
$oDimensionSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Dimensions"));
|
||||
$this->m_aDimensions = array();
|
||||
$this->m_aDimensions = array();
|
||||
while ($oDimension = $oDimensionSet->Fetch())
|
||||
{
|
||||
$this->m_aDimensions[$oDimension->GetKey()] = $oDimension;
|
||||
$this->m_aDimensions[$oDimension->GetKey()] = $oDimension;
|
||||
}
|
||||
|
||||
|
||||
$oClassProjSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ClassProjection"));
|
||||
$this->m_aClassProjs = array();
|
||||
$this->m_aClassProjs = array();
|
||||
while ($oClassProj = $oClassProjSet->Fetch())
|
||||
{
|
||||
$this->m_aClassProjs[$oClassProj->Get('class')][$oClassProj->Get('dimensionid')] = $oClassProj;
|
||||
$this->m_aClassProjs[$oClassProj->Get('class')][$oClassProj->Get('dimensionid')] = $oClassProj;
|
||||
}
|
||||
|
||||
$oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles"));
|
||||
$this->m_aProfiles = array();
|
||||
$this->m_aProfiles = array();
|
||||
while ($oProfile = $oProfileSet->Fetch())
|
||||
{
|
||||
$this->m_aProfiles[$oProfile->GetKey()] = $oProfile;
|
||||
$this->m_aProfiles[$oProfile->GetKey()] = $oProfile;
|
||||
}
|
||||
|
||||
$oUserProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserProfile"));
|
||||
@@ -676,10 +676,10 @@ class UserRightsProjection extends UserRightsAddOnAPI
|
||||
}
|
||||
|
||||
$oProProSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ProfileProjection"));
|
||||
$this->m_aProPros = array();
|
||||
$this->m_aProPros = array();
|
||||
while ($oProPro = $oProProSet->Fetch())
|
||||
{
|
||||
$this->m_aProPros[$oProPro->Get('profileid')][$oProPro->Get('dimensionid')] = $oProPro;
|
||||
$this->m_aProPros[$oProPro->Get('profileid')][$oProPro->Get('dimensionid')] = $oProPro;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -707,7 +707,7 @@ exit;
|
||||
// Authorize any for this dimension, then no additional criteria is required
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 1 - Get class projection info
|
||||
//
|
||||
$oExpression = null;
|
||||
@@ -731,13 +731,13 @@ exit;
|
||||
}
|
||||
elseif (strtolower(substr($sExpr, 0, 6)) == 'select')
|
||||
{
|
||||
throw new CoreException('Sorry, projections by the mean of OQL are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr));
|
||||
throw new CoreException('Sorry, projections by the mean of OQL are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Constant value(s)
|
||||
// unsupported
|
||||
throw new CoreException('Sorry, constant projections are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr));
|
||||
throw new CoreException('Sorry, constant projections are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr));
|
||||
// $aRes = explode(';', trim($sExpr));
|
||||
}
|
||||
|
||||
@@ -866,7 +866,7 @@ exit;
|
||||
$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode] = $aRes;
|
||||
return $aRes;
|
||||
}
|
||||
|
||||
|
||||
public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null)
|
||||
{
|
||||
if (is_null($oInstanceSet))
|
||||
@@ -934,7 +934,7 @@ exit;
|
||||
}
|
||||
else
|
||||
{
|
||||
$iInstancePermission = UR_ALLOWED_NO;
|
||||
$iInstancePermission = UR_ALLOWED_NO;
|
||||
}
|
||||
|
||||
if (isset($iGlobalPermission))
|
||||
@@ -1140,7 +1140,7 @@ exit;
|
||||
}
|
||||
|
||||
protected $m_aMatchingProfiles = array(); // cache of the matching profiles for a given user/object
|
||||
|
||||
|
||||
protected function GetMatchingProfiles($oUser, $sClass, /*DBObject*/ $oObject = null)
|
||||
{
|
||||
$iUser = $oUser->GetKey();
|
||||
@@ -1186,7 +1186,7 @@ exit;
|
||||
@$aProfileRes[$iProfile] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aRes = array();
|
||||
$iDimCount = count($this->m_aDimensions);
|
||||
foreach ($aProfileRes as $iProfile => $iMatches)
|
||||
@@ -1200,7 +1200,7 @@ exit;
|
||||
|
||||
// store into the cache
|
||||
$this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef] = $aRes;
|
||||
return $aRes;
|
||||
return $aRes;
|
||||
}
|
||||
|
||||
public function FlushPrivileges()
|
||||
|
||||
@@ -2248,43 +2248,25 @@ interface iModuleExtension
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to provide messages to be displayed in the "Welcome Popup"
|
||||
* KPI logging extensibility point
|
||||
*
|
||||
* @api
|
||||
* @private
|
||||
* @since 3.1.0
|
||||
* KPI Logger extension
|
||||
*/
|
||||
interface iWelcomePopup
|
||||
interface iKPILoggerExtension
|
||||
{
|
||||
// Importance for ordering messages
|
||||
// Just two levels since less important messages have nothing to do in the welcome popup
|
||||
const IMPORTANCE_CRITICAL = 0;
|
||||
const IMPORTANCE_HIGH = 1;
|
||||
/**
|
||||
* @return [['importance' => IMPORTANCE_CRITICAL|IMPORTANCE_HIGH, 'id' => '...', 'title' => '', 'html' => '', 'twig' => '']]
|
||||
*/
|
||||
public function GetMessages();
|
||||
/**
|
||||
* The message specified by the given Id has been acknowledged by the current user
|
||||
* @param string $sMessageId
|
||||
*/
|
||||
public function AcknowledgeMessage(string $sMessageId): void;
|
||||
}
|
||||
/**
|
||||
* Init the statistics collected
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function InitStats();
|
||||
|
||||
/**
|
||||
* Inherit from this class to provide messages to be displayed in the "Welcome Popup"
|
||||
*
|
||||
* @api
|
||||
* @since 3.1.0
|
||||
*/
|
||||
abstract class AbstractWelcomePopup implements iWelcomePopup
|
||||
{
|
||||
public function GetMessages()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
public function AcknowledgeMessage(string $sMessageId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a new KPI to the stats
|
||||
*
|
||||
* @param \Combodo\iTop\Core\Kpi\KpiLogData $oKpiLogData
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function LogOperation($oKpiLogData);
|
||||
}
|
||||
@@ -1166,7 +1166,7 @@ HTML
|
||||
/**
|
||||
* @param \WebPage $oPage
|
||||
* @param \CMDBObjectSet $oSet
|
||||
* @param array $aExtraParams
|
||||
* @param array $aExtraParams See possible values in {@see DataTableUIBlockFactory::RenderDataTable()}
|
||||
*
|
||||
* @throws \ApplicationException
|
||||
* @throws \CoreException
|
||||
@@ -4547,7 +4547,9 @@ HTML;
|
||||
foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) {
|
||||
$sExtensionClass = get_class($oExtensionInstance);
|
||||
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBInsert()");
|
||||
$oKPI = new ExecutionKPI();
|
||||
$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4562,13 +4564,16 @@ HTML;
|
||||
|
||||
protected function DBCloneTracked_Internal($newKey = null)
|
||||
{
|
||||
$oNewObj = parent::DBCloneTracked_Internal($newKey);
|
||||
/** @var cmdbAbstractObject $oNewObj */
|
||||
$oNewObj = MetaModel::GetObject(get_class($this), parent::DBCloneTracked_Internal($newKey));
|
||||
|
||||
// Invoke extensions after insertion (the object must exist, have an id, etc.)
|
||||
/** @var \iApplicationObjectExtension $oExtensionInstance */
|
||||
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
|
||||
}
|
||||
|
||||
return $oNewObj;
|
||||
@@ -4605,7 +4610,9 @@ HTML;
|
||||
foreach (MetaModel::EnumPlugins(iApplicationObjectExtension::class) as $oExtensionInstance) {
|
||||
$sExtensionClass = get_class($oExtensionInstance);
|
||||
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnDBUpdate()");
|
||||
$oKPI = new ExecutionKPI();
|
||||
$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBUpdate');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4649,7 +4656,9 @@ HTML;
|
||||
/** @var \iApplicationObjectExtension $oExtensionInstance */
|
||||
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete');
|
||||
}
|
||||
|
||||
return parent::DBDeleteTracked_Internal($oDeletionPlan);
|
||||
@@ -4668,7 +4677,10 @@ HTML;
|
||||
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
|
||||
{
|
||||
$sExtensionClass = get_class($oExtensionInstance);
|
||||
if ($oExtensionInstance->OnIsModified($this)) {
|
||||
$oKPI = new ExecutionKPI();
|
||||
$bIsModified = $oExtensionInstance->OnIsModified($this);
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified');
|
||||
if ($bIsModified) {
|
||||
$this->LogCRUDDebug(__METHOD__, "Calling $sExtensionClass::OnIsModified() -> true");
|
||||
return true;
|
||||
} else {
|
||||
@@ -4724,7 +4736,9 @@ HTML;
|
||||
/** @var \iApplicationObjectExtension $oExtensionInstance */
|
||||
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToWrite');
|
||||
if (is_array($aNewIssues) && (count($aNewIssues) > 0)) // Some extensions return null instead of an empty array
|
||||
{
|
||||
$this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues);
|
||||
@@ -4772,7 +4786,9 @@ HTML;
|
||||
/** @var \iApplicationObjectExtension $oExtensionInstance */
|
||||
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
|
||||
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToDelete');
|
||||
if (is_array($aNewIssues) && count($aNewIssues) > 0)
|
||||
{
|
||||
$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues);
|
||||
|
||||
@@ -918,6 +918,11 @@ class RuntimeDashboard extends Dashboard
|
||||
{
|
||||
$bCustomized = false;
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
@@ -929,7 +934,7 @@ class RuntimeDashboard extends Dashboard
|
||||
$sDashboardDefinition = $oUserDashboard->Get('contents');
|
||||
$bCustomized = true;
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFile);
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
|
||||
|
||||
@@ -937,7 +942,7 @@ class RuntimeDashboard extends Dashboard
|
||||
$oDashboard = new RuntimeDashboard($sDashBoardId);
|
||||
$oDashboard->FromXml($sDashboardDefinition);
|
||||
$oDashboard->SetCustomFlag($bCustomized);
|
||||
$oDashboard->SetDefinitionFile($sDashboardFile);
|
||||
$oDashboard->SetDefinitionFile($sDashboardFileSanitized);
|
||||
} else {
|
||||
$oDashboard = null;
|
||||
}
|
||||
|
||||
@@ -40,36 +40,6 @@
|
||||
<presentation/>
|
||||
<methods/>
|
||||
</class>
|
||||
<class id="WelcomePopupAcknowledge" _delta="define">
|
||||
<parent>DBObject</parent>
|
||||
<properties>
|
||||
<comment>/* Acknowledge welcome popup messages */</comment>
|
||||
<abstract>false</abstract>
|
||||
<category></category>
|
||||
<key_type>autoincrement</key_type>
|
||||
<db_table>priv_welcome_popup_acknowledge</db_table>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="message_uuid" xsi:type="AttributeString">
|
||||
<sql>message_uuid</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
<field id="user_id" xsi:type="AttributeExternalKey">
|
||||
<sql>user_id</sql>
|
||||
<target_class>User</target_class>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
<on_target_delete>DEL_SILENT</on_target_delete>
|
||||
</field>
|
||||
<field id="acknowledge_date" xsi:type="AttributeDateTime">
|
||||
<sql>acknowledge_date</sql>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
</fields>
|
||||
<presentation/>
|
||||
<methods/>
|
||||
</class>
|
||||
</classes>
|
||||
<portals>
|
||||
<portal id="backoffice" _delta="define">
|
||||
|
||||
@@ -9,7 +9,6 @@ use Combodo\iTop\Application\Helper\WebResourcesHelper;
|
||||
require_once(APPROOT.'/application/utils.inc.php');
|
||||
require_once(APPROOT.'/application/template.class.inc.php');
|
||||
require_once(APPROOT."/application/user.dashboard.class.inc.php");
|
||||
require_once(APPROOT."/setup/parentmenunodecompiler.class.inc.php");
|
||||
|
||||
|
||||
/**
|
||||
@@ -104,7 +103,7 @@ class ApplicationMenu
|
||||
{
|
||||
self::$sFavoriteSiloQuery = $sOQL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query used to limit the list of displayed organizations in the drop-down menu
|
||||
* @return string The OQL query returning a list of Organization objects
|
||||
@@ -274,23 +273,12 @@ class ApplicationMenu
|
||||
continue;
|
||||
}
|
||||
|
||||
$aSubMenuNodes = static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams);
|
||||
if (! ParentMenuNodeCompiler::$bUseLegacyMenuCompilation && !($oMenuNode instanceof ShortcutMenuNode)){
|
||||
if (is_array($aSubMenuNodes) && 0 === sizeof($aSubMenuNodes)){
|
||||
IssueLog::Error('Empty menu node not displayed', LogChannels::CONSOLE, [
|
||||
'menu_node_class' => get_class($oMenuNode),
|
||||
'menu_node_label' => $oMenuNode->GetLabel(),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$aMenuGroups[] = [
|
||||
'sId' => $oMenuNode->GetMenuID(),
|
||||
'sIconCssClasses' => $oMenuNode->GetDecorationClasses(),
|
||||
'sInitials' => $oMenuNode->GetInitials(),
|
||||
'sTitle' => $oMenuNode->GetTitle(),
|
||||
'aSubMenuNodes' => $aSubMenuNodes,
|
||||
'aSubMenuNodes' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -548,7 +536,7 @@ EOF
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the currently active menu (if any, otherwise the first menu is the default)
|
||||
* @return string The Id of the currently active menu
|
||||
@@ -556,7 +544,7 @@ EOF
|
||||
public static function GetActiveNodeId()
|
||||
{
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
if ($sMenuId === null)
|
||||
{
|
||||
$sMenuId = self::GetDefaultMenuId();
|
||||
@@ -666,7 +654,7 @@ abstract class MenuNode
|
||||
|
||||
/**
|
||||
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
|
||||
*/
|
||||
*/
|
||||
protected $m_aEnableStimuli;
|
||||
|
||||
/**
|
||||
@@ -826,7 +814,7 @@ abstract class MenuNode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a limiting display condition for the same menu node. The conditions will be combined with a AND
|
||||
* @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction
|
||||
@@ -999,7 +987,7 @@ class TemplateMenuNode extends MenuNode
|
||||
* @var string
|
||||
*/
|
||||
protected $sTemplateFile;
|
||||
|
||||
|
||||
/**
|
||||
* Create a menu item based on a custom template and inserts it into the application's main menu
|
||||
* @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary)
|
||||
@@ -1070,7 +1058,7 @@ class OQLMenuNode extends MenuNode
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $bSearchFormOpen;
|
||||
|
||||
|
||||
/**
|
||||
* Extra parameters to be passed to the display block to fine tune its appearence
|
||||
*/
|
||||
@@ -1103,7 +1091,7 @@ class OQLMenuNode extends MenuNode
|
||||
// Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects
|
||||
// of the class specified by the OQL...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set some extra parameters to be passed to the display block to fine tune its appearence
|
||||
* @param array $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters
|
||||
@@ -1132,7 +1120,7 @@ class OQLMenuNode extends MenuNode
|
||||
'Menu_'.$this->GetMenuId(),
|
||||
$this->bSearch, // Search pane
|
||||
$this->bSearchFormOpen, // Search open
|
||||
$oPage,
|
||||
$oPage,
|
||||
array_merge($this->m_aParams, $aExtraParams),
|
||||
true
|
||||
);
|
||||
@@ -1366,10 +1354,10 @@ class NewObjectMenuNode extends MenuNode
|
||||
{
|
||||
// Enable this menu, only if the current user has enough rights to create such an object, or an object of
|
||||
// any child class
|
||||
|
||||
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
|
||||
$bActionIsAllowed = false;
|
||||
|
||||
|
||||
foreach($aSubClasses as $sCandidateClass)
|
||||
{
|
||||
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
|
||||
@@ -1378,7 +1366,7 @@ class NewObjectMenuNode extends MenuNode
|
||||
break; // Enough for now
|
||||
}
|
||||
}
|
||||
return $bActionIsAllowed;
|
||||
return $bActionIsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1520,7 +1508,7 @@ class DashboardMenuNode extends MenuNode
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1561,7 +1549,7 @@ class ShortcutContainerMenuNode extends MenuNode
|
||||
$sName = $this->GetMenuId().'_'.$oShortcut->GetKey();
|
||||
new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++);
|
||||
}
|
||||
|
||||
|
||||
// Complete the tree
|
||||
//
|
||||
parent::PopulateChildMenus();
|
||||
|
||||
@@ -99,4 +99,10 @@ else
|
||||
Session::Set('itop_env', ITOP_DEFAULT_ENV);
|
||||
}
|
||||
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
|
||||
try {
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
|
||||
}
|
||||
catch (MySQLException $e) {
|
||||
IssueLog::Debug($e->getMessage());
|
||||
throw new MySQLException('Could not connect to the DB server', []);
|
||||
}
|
||||
29
application/templates/welcome_popup.html
Normal file
29
application/templates/welcome_popup.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<div style="width:100%;background: #fff url(../images/welcome.jpg) top left no-repeat;">
|
||||
<style>
|
||||
.welcome_popup_cell {
|
||||
vertical-align:top;
|
||||
width:50%;
|
||||
border:0px solid #000;
|
||||
-moz-border-radius:10px;
|
||||
padding:5px;
|
||||
text-align:left;
|
||||
}
|
||||
tr td.welcome_popup_cell, tr td.welcome_popup_cell ul {
|
||||
font-size:10pt;
|
||||
}
|
||||
</style>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p style="text-align:left; font-size:32px;padding-left:400px;padding-top:40px;margin-bottom:30px;margin-top:0;color:#FFFFFF;"><itopstring>UI:WelcomeMenu:Title</itopstring></p>
|
||||
<p></p>
|
||||
<table border="0" style="padding:10px;border-spacing: 10px;width:100%">
|
||||
<tr>
|
||||
<td class="welcome_popup_cell">
|
||||
<itopstring>UI:WelcomeMenu:LeftBlock</itopstring>
|
||||
</td>
|
||||
<td class="welcome_popup_cell">
|
||||
<itopstring>UI:WelcomeMenu:RightBlock</itopstring>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -20,6 +20,7 @@
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Application\UI\Base\iUIBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Service\Module\ModuleService;
|
||||
use ScssPhp\ScssPhp\Compiler;
|
||||
use ScssPhp\ScssPhp\OutputStyle;
|
||||
use ScssPhp\ScssPhp\ValueConverter;
|
||||
@@ -1396,13 +1397,23 @@ class utils
|
||||
return APPROOT . 'env-' . MetaModel::GetEnvironment() . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string A path to the folder into which data can be written
|
||||
* @internal
|
||||
* @since N°6097 2.7.10 3.0.4 3.1.1
|
||||
*/
|
||||
public static function GetDataPath(): string
|
||||
{
|
||||
return APPROOT.'data/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string A path to a folder into which any module can store cache data
|
||||
* The corresponding folder is created or cleaned upon code compilation
|
||||
*/
|
||||
public static function GetCachePath()
|
||||
{
|
||||
return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/';
|
||||
return static::GetDataPath().'cache-'.MetaModel::GetEnvironment().'/';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2265,24 +2276,7 @@ SQL;
|
||||
*/
|
||||
public static function GetCurrentModuleName($iCallDepth = 0)
|
||||
{
|
||||
$sCurrentModuleName = '';
|
||||
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
|
||||
|
||||
foreach(GetModulesInfo() as $sModuleName => $aInfo)
|
||||
{
|
||||
if ($aInfo['root_dir'] !== '')
|
||||
{
|
||||
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
|
||||
|
||||
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
|
||||
{
|
||||
$sCurrentModuleName = $sModuleName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sCurrentModuleName;
|
||||
return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2304,24 +2298,7 @@ SQL;
|
||||
*/
|
||||
public static function GetCurrentModuleDir($iCallDepth)
|
||||
{
|
||||
$sCurrentModuleDir = '';
|
||||
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
|
||||
|
||||
foreach(GetModulesInfo() as $sModuleName => $aInfo)
|
||||
{
|
||||
if ($aInfo['root_dir'] !== '')
|
||||
{
|
||||
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
|
||||
|
||||
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
|
||||
{
|
||||
$sCurrentModuleDir = basename($sRootDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sCurrentModuleDir;
|
||||
return ModuleService::GetInstance()->GetCurrentModuleDir($iCallDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2336,12 +2313,7 @@ SQL;
|
||||
*/
|
||||
public static function GetCurrentModuleUrl()
|
||||
{
|
||||
$sDir = static::GetCurrentModuleDir(1);
|
||||
if ( $sDir !== '')
|
||||
{
|
||||
return static::GetAbsoluteUrlModulesRoot().'/'.$sDir;
|
||||
}
|
||||
return '';
|
||||
return ModuleService::GetInstance()->GetCurrentModuleUrl(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2351,8 +2323,7 @@ SQL;
|
||||
*/
|
||||
public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null)
|
||||
{
|
||||
$sModuleName = static::GetCurrentModuleName(1);
|
||||
return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue);
|
||||
return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2361,12 +2332,7 @@ SQL;
|
||||
*/
|
||||
public static function GetCompiledModuleVersion($sModuleName)
|
||||
{
|
||||
$aModulesInfo = GetModulesInfo();
|
||||
if (array_key_exists($sModuleName, $aModulesInfo))
|
||||
{
|
||||
return $aModulesInfo[$sModuleName]['version'];
|
||||
}
|
||||
return null;
|
||||
return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2691,24 +2657,26 @@ SQL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local path relative to the iTop installation of an existing file
|
||||
* Returns the local path relative to the iTop installation (APPROOT or the given base path)
|
||||
* Dir separator is changed to '/' for consistency among the different OS
|
||||
*
|
||||
* @param string $sAbsolutePath absolute path
|
||||
* @param string $sBasePath Base path for the resulting local path (default APPROOT)
|
||||
*
|
||||
* @return false|string
|
||||
* @return false|string The generated local path or false if absolute path is not under the base path
|
||||
* @since 3.1.1 Added base path defaulted to previous version APPROOT
|
||||
*/
|
||||
final public static function LocalPath($sAbsolutePath)
|
||||
final public static function LocalPath($sAbsolutePath, string $sBasePath = APPROOT)
|
||||
{
|
||||
$sRootPath = realpath(APPROOT);
|
||||
$sRootPath = realpath($sBasePath);
|
||||
$sFullPath = realpath($sAbsolutePath);
|
||||
if (($sFullPath === false) || !self::StartsWith($sFullPath, $sRootPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
$sLocalPath = substr($sFullPath, strlen($sRootPath.DIRECTORY_SEPARATOR));
|
||||
$sLocalPath = str_replace(DIRECTORY_SEPARATOR, '/', $sLocalPath);
|
||||
return $sLocalPath;
|
||||
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', $sLocalPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2900,7 +2868,7 @@ HTML;
|
||||
|
||||
// Add already loaded classes
|
||||
$aCurrentClasses = array_fill_keys(get_declared_classes(), '');
|
||||
$aClassMap = array_merge($aClassMap, $aCurrentClasses);
|
||||
$aClassMap = array_merge($aCurrentClasses, $aClassMap);
|
||||
|
||||
foreach ($aClassMap as $sPHPClass => $sPHPFile) {
|
||||
$bSkipped = false;
|
||||
@@ -2925,11 +2893,12 @@ HTML;
|
||||
$bSkipped = true; // file not found
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(!$bSkipped){
|
||||
try {
|
||||
$oRefClass = new ReflectionClass($sPHPClass);
|
||||
if ($oRefClass->implementsInterface($sInterface) && $oRefClass->isInstantiable()) {
|
||||
if ($oRefClass->implementsInterface($sInterface) &&
|
||||
!$oRefClass->isInterface() && !$oRefClass->isAbstract() && !$oRefClass->isTrait()) {
|
||||
$aMatchingClasses[] = $sPHPClass;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -45,6 +45,7 @@ define('MAINTENANCE_MODE_FILE', APPROOT.'data/.maintenance');
|
||||
define('READONLY_MODE_FILE', APPROOT.'data/.readonly');
|
||||
|
||||
$fItopStarted = microtime(true);
|
||||
$iItopInitialMemory = memory_get_usage(true);
|
||||
|
||||
if (!isset($GLOBALS['bBypassAutoload']) || $GLOBALS['bBypassAutoload'] == false) {
|
||||
require_once APPROOT.'/lib/autoload.php';
|
||||
|
||||
@@ -59,9 +59,16 @@ class DbConnectionWrapper
|
||||
* Use this to register a mock that will handle {@see mysqli::query()}
|
||||
*
|
||||
* @param \mysqli|null $oMysqli
|
||||
* @since 3.0.4 3.1.1 3.2.0 Param $oMysqli becomes nullable
|
||||
*/
|
||||
public static function SetDbConnectionMockForQuery(?mysqli $oMysqli): void
|
||||
public static function SetDbConnectionMockForQuery(?mysqli $oMysqli = null): void
|
||||
{
|
||||
static::$oDbCnxMockableForQuery = $oMysqli;
|
||||
if (is_null($oMysqli)) {
|
||||
// Reset to standard connection
|
||||
static::$oDbCnxMockableForQuery = static::$oDbCnxStandard;
|
||||
}
|
||||
else {
|
||||
static::$oDbCnxMockableForQuery = $oMysqli;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,6 +419,7 @@ class MyHelpers
|
||||
//}
|
||||
return $sOutput;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,5 +524,3 @@ class Str
|
||||
return (strtolower($sString) == $sString);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -431,6 +431,7 @@ class CMDBSource
|
||||
{
|
||||
self::$m_sDBName = '';
|
||||
}
|
||||
self::_TablesInfoCacheReset(); // reset the table info cache!
|
||||
}
|
||||
|
||||
public static function CreateTable($sQuery)
|
||||
@@ -607,8 +608,9 @@ class CMDBSource
|
||||
{
|
||||
self::LogDeadLock($e, true);
|
||||
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
|
||||
}
|
||||
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
|
||||
} finally {
|
||||
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
|
||||
}
|
||||
if ($oResult === false) {
|
||||
$aContext = array('query' => $sSql);
|
||||
|
||||
@@ -626,18 +628,24 @@ class CMDBSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Exception $e
|
||||
* @param Exception $e
|
||||
* @param bool $bForQuery to get the proper DB connection
|
||||
* @param bool $bCheckMysqliErrno if false won't try to check for mysqli::errno value
|
||||
*
|
||||
* @since 2.7.1
|
||||
* @since 3.0.0 N°4325 add new optional parameter to use the correct DB connection
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6643 new bCheckMysqliErrno parameter as a workaround for mysqli::errno cannot be mocked
|
||||
*/
|
||||
private static function LogDeadLock(Exception $e, $bForQuery = false)
|
||||
private static function LogDeadLock(Exception $e, $bForQuery = false, $bCheckMysqliErrno = true)
|
||||
{
|
||||
// checks MySQL error code
|
||||
$iMySqlErrorNo = DbConnectionWrapper::GetDbConnection($bForQuery)->errno;
|
||||
if (!in_array($iMySqlErrorNo, array(self::MYSQL_ERRNO_WAIT_TIMEOUT, self::MYSQL_ERRNO_DEADLOCK))) {
|
||||
return;
|
||||
if ($bCheckMysqliErrno) {
|
||||
$iMySqlErrorNo = DbConnectionWrapper::GetDbConnection($bForQuery)->errno;
|
||||
if (!in_array($iMySqlErrorNo, array(self::MYSQL_ERRNO_WAIT_TIMEOUT, self::MYSQL_ERRNO_DEADLOCK))) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$iMySqlErrorNo = "N/A";
|
||||
}
|
||||
|
||||
// Get error info
|
||||
@@ -664,7 +672,10 @@ class CMDBSource
|
||||
);
|
||||
DeadLockLog::Info($sMessage, $iMySqlErrorNo, $aLogContext);
|
||||
|
||||
IssueLog::Error($sMessage, LogChannels::DEADLOCK, $e->getMessage());
|
||||
IssueLog::Error($sMessage, LogChannels::DEADLOCK, [
|
||||
'exception.class' => get_class($e),
|
||||
'exception.message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ define('ITOP_APPLICATION_SHORT', 'iTop');
|
||||
*
|
||||
* @see ITOP_CORE_VERSION to get iTop core version
|
||||
*/
|
||||
define('ITOP_VERSION', '3.1.0-dev');
|
||||
define('ITOP_VERSION', '3.1.1-dev');
|
||||
|
||||
define('ITOP_VERSION_NAME', 'Fullmoon');
|
||||
define('ITOP_REVISION', 'svn');
|
||||
@@ -656,22 +656,22 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'email_transport_smtp.allow_self_signed' => array(
|
||||
'email_transport_smtp.allow_self_signed' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Allow self signed peer certificates',
|
||||
'default' => false,
|
||||
'value' => false,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'email_transport_smtp.verify_peer' => array(
|
||||
],
|
||||
'email_transport_smtp.verify_peer' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Verify peer certificate',
|
||||
'default' => true,
|
||||
'value' => true,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
],
|
||||
'email_css' => [
|
||||
'type' => 'string',
|
||||
'description' => 'CSS that will override the standard stylesheet used for the notifications',
|
||||
@@ -1069,6 +1069,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'log_kpi_generate_legacy_report' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Generate the legacy KPI report (kpi.html)',
|
||||
'default' => true,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'max_linkset_output' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.',
|
||||
@@ -1345,14 +1353,6 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'navigation_menu.sorted_popup_user_menu_items' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Sort user menu items after setup on page load',
|
||||
'default' => [],
|
||||
'value' => false,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'quick_create.enabled' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not the quick create is enabled',
|
||||
@@ -1635,14 +1635,6 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.single_profile_completion' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Non standalone profiles can be completed by other profiles via this configuration. default configuration is equivalent to [\'Portal power user\' => \'Portal user\'] configuration. unless you have specific portal customization.',
|
||||
'default' => null,
|
||||
'value' => false,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'behind_reverse_proxy' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is only here for compatibility issues. Will be removed in iTop 3.1.0 (N°3664)
|
||||
* This file is only here for compatibility reasons.
|
||||
* It will be removed in future iTop versions (N°6533)
|
||||
*
|
||||
* @deprecated 3.0.0 N°3663 Exception classes were moved to `/application/exceptions`, use autoloader instead of require !
|
||||
*/
|
||||
require_once '../approot.inc.php';
|
||||
DeprecatedCallsLog::NotifyDeprecatedFile('Classes were moved to /application/exceptions');
|
||||
|
||||
require_once __DIR__ . '/../approot.inc.php';
|
||||
|
||||
DeprecatedCallsLog::NotifyDeprecatedFile('Classes were moved to /application/exceptions and can be used directly with the autoloader');
|
||||
|
||||
@@ -188,8 +188,8 @@ final class ItopCounter
|
||||
|
||||
if (!$hDBLink)
|
||||
{
|
||||
throw new Exception("Could not connect to the DB server (host=$sDBHost, user=$sDBUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
|
||||
}
|
||||
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
|
||||
}
|
||||
|
||||
return $hDBLink;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ require_once('mutex.class.inc.php');
|
||||
|
||||
|
||||
/**
|
||||
* A persistent object, as defined by the metamodel
|
||||
* A persistent object, as defined by the metamodel
|
||||
*
|
||||
* @package iTopORM
|
||||
* @api
|
||||
@@ -299,9 +299,9 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Whether the object is already persisted in DB or not.
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsNew()
|
||||
@@ -311,9 +311,9 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Returns an Id for memory objects
|
||||
*
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
*
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return int
|
||||
@@ -350,7 +350,7 @@ abstract class DBObject implements iDisplay
|
||||
$sRet .= "<b title=\"$sRootClass\">$sClass</b>::$iPKey ($sFriendlyname)<br/>\n";
|
||||
return $sRet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alias of DBObject::Reload()
|
||||
*
|
||||
@@ -373,7 +373,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* @internal
|
||||
* @see m_bFullyLoaded
|
||||
*
|
||||
*
|
||||
* @return bool
|
||||
* @throws CoreException
|
||||
*/
|
||||
@@ -496,7 +496,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
$aAttList = $aAttToLoad[$sClassAlias];
|
||||
}
|
||||
|
||||
|
||||
foreach($aAttList as $sAttCode=>$oAttDef)
|
||||
{
|
||||
// Skip links (could not be loaded by the mean of this query)
|
||||
@@ -556,7 +556,7 @@ abstract class DBObject implements iDisplay
|
||||
$bFullyLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load extended data
|
||||
if ($aExtendedDataSpec != null)
|
||||
{
|
||||
@@ -580,7 +580,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* @internal
|
||||
* @see Set()
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
* @param mixed $value
|
||||
*/
|
||||
@@ -785,11 +785,11 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Get the label of an attribute.
|
||||
*
|
||||
*
|
||||
* Shortcut to the field's AttributeDefinition->GetLabel()
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
*
|
||||
* @return string
|
||||
@@ -862,7 +862,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* @internal
|
||||
* @see Get
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
*
|
||||
* @return int|mixed|null
|
||||
@@ -967,7 +967,7 @@ abstract class DBObject implements iDisplay
|
||||
* Returns the default value of the $sAttCode.
|
||||
*
|
||||
* Returns the default value of the given attribute.
|
||||
*
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param string $sAttCode
|
||||
@@ -988,12 +988,12 @@ abstract class DBObject implements iDisplay
|
||||
* @internal
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
*/
|
||||
public function GetExtendedData()
|
||||
{
|
||||
return $this->m_aExtendedData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the HighlightCode
|
||||
*
|
||||
@@ -1015,7 +1015,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
$fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank'];
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists($sCode, $aHighlightScale))
|
||||
{
|
||||
$fRank = $aHighlightScale[$sCode]['rank'];
|
||||
@@ -1025,13 +1025,13 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current HighlightCode
|
||||
*
|
||||
*
|
||||
* @internal
|
||||
* @used-by DBObject::ComputeHighlightCode()
|
||||
*
|
||||
*
|
||||
* @return string|null The Hightlight code (null if none set, meaning rank = 0)
|
||||
*/
|
||||
protected function GetHighlightCode()
|
||||
@@ -1080,7 +1080,7 @@ abstract class DBObject implements iDisplay
|
||||
* corresponding to the external key and getting the value from it
|
||||
*
|
||||
* UNUSED ?
|
||||
*
|
||||
*
|
||||
* @internal
|
||||
* @todo: check if this is dead code.
|
||||
*
|
||||
@@ -1144,12 +1144,14 @@ abstract class DBObject implements iDisplay
|
||||
return; //skip!
|
||||
}
|
||||
$this->FireEventComputeValues();
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->ComputeValues();
|
||||
$oKPI->ComputeStatsForExtension($this, 'ComputeValues');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
* @param bool $bLocalize
|
||||
*
|
||||
@@ -1195,11 +1197,11 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Get the value as it must be in the edit areas (forms)
|
||||
*
|
||||
*
|
||||
* Makes a raw text representation of the value.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
*
|
||||
* @return int|mixed|string
|
||||
@@ -1229,7 +1231,7 @@ abstract class DBObject implements iDisplay
|
||||
else
|
||||
{
|
||||
$sEditValue = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1245,14 +1247,14 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Get $sAttCode formatted as XML
|
||||
*
|
||||
*
|
||||
* The returned value is a text that is suitable for insertion into an XML node.
|
||||
* Depending on the type of attribute, the returned text is either:
|
||||
* * A literal, with XML entities already escaped,
|
||||
* * XML
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
* @param bool $bLocalize
|
||||
*
|
||||
@@ -1290,10 +1292,10 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @see GetAsHTML()
|
||||
* @see GetOriginal()
|
||||
*
|
||||
*
|
||||
* @param string $sAttCode
|
||||
* @param bool $bLocalize
|
||||
*
|
||||
@@ -1468,7 +1470,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
*
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return mixed
|
||||
@@ -1523,7 +1525,7 @@ abstract class DBObject implements iDisplay
|
||||
* Get the id
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetKey()
|
||||
@@ -1534,7 +1536,7 @@ abstract class DBObject implements iDisplay
|
||||
/**
|
||||
* Primary key Setter
|
||||
* Usable only for not yet persisted DBObjects
|
||||
*
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param int $iNewKey the desired identifier
|
||||
@@ -1547,7 +1549,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
throw new CoreException("An object id must be an integer value ($iNewKey)");
|
||||
}
|
||||
|
||||
|
||||
if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey))
|
||||
{
|
||||
throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database");
|
||||
@@ -1557,7 +1559,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Get the icon representing this object
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param boolean $bImgTag If true the result is a full IMG tag (or an empty string if no icon is defined)
|
||||
@@ -1647,7 +1649,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* Returns the label as defined in the dictionary for the language of the current user
|
||||
*
|
||||
* @api
|
||||
* @api
|
||||
*
|
||||
* @return string (empty for default name scheme)
|
||||
*/
|
||||
@@ -1714,7 +1716,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Helper to get the state
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return mixed|string '' if no state attribute, object representing its value otherwise
|
||||
@@ -1736,9 +1738,9 @@ abstract class DBObject implements iDisplay
|
||||
/**
|
||||
* Get the label (raw text) of the current state
|
||||
* helper for MetaModel::GetStateLabel()
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*
|
||||
* @return mixed|string
|
||||
*
|
||||
* @throws ArchivedObjectException
|
||||
@@ -1785,7 +1787,7 @@ abstract class DBObject implements iDisplay
|
||||
* Define attributes read-only from the end-user perspective
|
||||
*
|
||||
* @return array|null List of attcodes
|
||||
*/
|
||||
*/
|
||||
public static function GetReadOnlyAttributes()
|
||||
{
|
||||
return null;
|
||||
@@ -1794,14 +1796,14 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Get predefined objects
|
||||
*
|
||||
*
|
||||
* The predefined objects will be synchronized with the DB at each install/upgrade
|
||||
* As soon as a class has predefined objects, then nobody can create nor delete objects
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.)
|
||||
*/
|
||||
*/
|
||||
public static function GetPredefinedObjects()
|
||||
{
|
||||
return null;
|
||||
@@ -1930,7 +1932,7 @@ abstract class DBObject implements iDisplay
|
||||
* Note: Attributes (and flags) from the target state and the transition are combined.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
*
|
||||
* @param string $sStimulus
|
||||
* @param string $sOriginState Default is current state
|
||||
*
|
||||
@@ -2137,7 +2139,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \OQLException
|
||||
*
|
||||
@@ -2480,7 +2482,6 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
$this->m_aCheckIssues = array();
|
||||
|
||||
$oKPI = new ExecutionKPI();
|
||||
if ($bDoComputeValues) {
|
||||
$this->DoComputeValues();
|
||||
}
|
||||
@@ -2490,8 +2491,9 @@ abstract class DBObject implements iDisplay
|
||||
$this->FireEventCheckToWrite();
|
||||
$this->SetReadWrite();
|
||||
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->DoCheckToWrite();
|
||||
$oKPI->ComputeStats('CheckToWrite', get_class($this));
|
||||
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
|
||||
if (count($this->m_aCheckIssues) == 0)
|
||||
{
|
||||
$this->m_bCheckStatus = true;
|
||||
@@ -2509,7 +2511,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* an array of displayable error is added in {@see DBObject::$m_aDeleteIssues}
|
||||
*
|
||||
* @internal
|
||||
* @internal
|
||||
*
|
||||
* @param \DeletionPlan $oDeletionPlan
|
||||
*
|
||||
@@ -2626,7 +2628,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
// The value is a scalar, the comparison must be 100% strict
|
||||
if($this->m_aOrigValues[$sAtt] !== $proposedValue)
|
||||
{
|
||||
{
|
||||
//echo "$sAtt:<pre>\n";
|
||||
//var_dump($this->m_aOrigValues[$sAtt]);
|
||||
//var_dump($proposedValue);
|
||||
@@ -2748,7 +2750,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Used only by insert, Meant to be overloaded
|
||||
*
|
||||
*
|
||||
* @overwritable-hook You can extend this method in order to provide your own logic.
|
||||
*/
|
||||
protected function OnObjectKeyReady()
|
||||
@@ -2856,7 +2858,7 @@ abstract class DBObject implements iDisplay
|
||||
// fields in first array, values in the second
|
||||
$aFieldsToWrite = array();
|
||||
$aValuesToWrite = array();
|
||||
|
||||
|
||||
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
|
||||
{
|
||||
// Add it to the list of fields to write
|
||||
@@ -2865,7 +2867,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
|
||||
$aHierarchicalKeys = array();
|
||||
|
||||
|
||||
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) {
|
||||
// Skip this attribute if not defined in this table
|
||||
if ((!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode) && !$oAttDef->CopyOnAllTables())
|
||||
@@ -2875,7 +2877,7 @@ abstract class DBObject implements iDisplay
|
||||
$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
|
||||
foreach($aAttColumns as $sColumn => $sValue)
|
||||
{
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aValuesToWrite[] = CMDBSource::Quote($sValue);
|
||||
}
|
||||
if ($oAttDef->IsHierarchicalKey())
|
||||
@@ -2899,7 +2901,7 @@ abstract class DBObject implements iDisplay
|
||||
self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite);
|
||||
}
|
||||
self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')';
|
||||
|
||||
|
||||
$iNewKey = 999999; // TODO - compute next id....
|
||||
}
|
||||
else
|
||||
@@ -2984,7 +2986,7 @@ abstract class DBObject implements iDisplay
|
||||
// fields in first array, values in the second
|
||||
$aFieldsToWrite = array();
|
||||
$aValuesToWrite = array();
|
||||
|
||||
|
||||
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
|
||||
{
|
||||
// Add it to the list of fields to write
|
||||
@@ -3019,7 +3021,7 @@ abstract class DBObject implements iDisplay
|
||||
$aAttColumns = $oAttDef->GetSQLValues($value);
|
||||
foreach($aAttColumns as $sColumn => $sValue)
|
||||
{
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aValuesToWrite[] = CMDBSource::Quote($sValue);
|
||||
}
|
||||
if ($oAttDef->IsHierarchicalKey())
|
||||
@@ -3114,7 +3116,9 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
// Ensure the update of the values (we are accessing the data directly)
|
||||
$this->DoComputeValues();
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->OnInsert();
|
||||
$oKPI->ComputeStatsForExtension($this, 'OnInsert');
|
||||
|
||||
$this->FireEventBeforeWrite();
|
||||
|
||||
@@ -3157,10 +3161,6 @@ abstract class DBObject implements iDisplay
|
||||
// First query built upon on the root class, because the ID must be created first
|
||||
$this->m_iKey = $this->DBInsertSingleTable($sRootClass);
|
||||
|
||||
//since N°5324: issue with test and db links events
|
||||
$this->SetReadOnly('No modification allowed during transaction');
|
||||
MetaModel::StartReentranceProtection($this);
|
||||
|
||||
// Then do the leaf class, if different from the root class
|
||||
if ($sClass != $sRootClass) {
|
||||
$this->DBInsertSingleTable($sClass);
|
||||
@@ -3174,7 +3174,9 @@ abstract class DBObject implements iDisplay
|
||||
$this->DBInsertSingleTable($sParentClass);
|
||||
}
|
||||
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->OnObjectKeyReady();
|
||||
$oKPI->ComputeStatsForExtension($this, 'OnObjectKeyReady');
|
||||
$this->UpdateCurrentObjectInCrudStack();
|
||||
|
||||
$this->DBWriteLinks();
|
||||
@@ -3210,7 +3212,6 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
$this->SetReadWrite();
|
||||
$this->m_bIsInDB = true;
|
||||
$this->m_bDirty = false;
|
||||
foreach ($this->m_aCurrValues as $sAttCode => $value) {
|
||||
@@ -3220,6 +3221,9 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_aOrigValues[$sAttCode] = $value;
|
||||
}
|
||||
|
||||
// Prevent DBUpdate at this point (reentrance protection)
|
||||
MetaModel::StartReentranceProtection($this);
|
||||
|
||||
try {
|
||||
$this->PostInsertActions();
|
||||
}
|
||||
@@ -3249,7 +3253,9 @@ abstract class DBObject implements iDisplay
|
||||
public function PostInsertActions(): void
|
||||
{
|
||||
$this->FireEventAfterWrite([], true);
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->AfterInsert();
|
||||
$oKPI->ComputeStatsForExtension($this, 'AfterInsert');
|
||||
|
||||
// Activate any existing trigger
|
||||
$sClass = get_class($this);
|
||||
@@ -3295,7 +3301,7 @@ abstract class DBObject implements iDisplay
|
||||
$this->RecordObjCreation();
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is automatically called after cloning an object with the "clone" PHP language construct
|
||||
* The purpose of this method is to reset the appropriate attributes of the object in
|
||||
@@ -3347,7 +3353,9 @@ abstract class DBObject implements iDisplay
|
||||
try {
|
||||
$this->DoComputeValues();
|
||||
$this->ComputeStopWatchesDeadline(false);
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->OnUpdate();
|
||||
$oKPI->ComputeStatsForExtension($this, 'OnUpdate');
|
||||
|
||||
$this->FireEventBeforeWrite();
|
||||
|
||||
@@ -3384,7 +3392,6 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
$this->SetReadOnly('No modification allowed during transaction');
|
||||
$iTransactionRetry = 1;
|
||||
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
|
||||
if ($bIsTransactionEnabled) {
|
||||
@@ -3499,8 +3506,6 @@ abstract class DBObject implements iDisplay
|
||||
// following lines are resetting changes (so after this {@see DBObject::ListChanges()} won't return changes anymore)
|
||||
// new values are already in the object (call {@see DBObject::Get()} to get them)
|
||||
// call {@see DBObject::ListPreviousValuesForUpdatedAttributes()} to get changed fields and previous values
|
||||
|
||||
$this->SetReadWrite();
|
||||
$this->m_bDirty = false;
|
||||
$this->m_aTouchedAtt = array();
|
||||
$this->m_aModifiedAtt = array();
|
||||
@@ -3564,10 +3569,13 @@ abstract class DBObject implements iDisplay
|
||||
public function PostUpdateActions(array $aChanges): void
|
||||
{
|
||||
$this->FireEventAfterWrite($aChanges, false);
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->AfterUpdate();
|
||||
$oKPI->ComputeStatsForExtension($this, 'AfterUpdate');
|
||||
|
||||
// - TriggerOnObjectUpdate
|
||||
$aParams = array('class_list' => MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL));
|
||||
$aClassList = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
|
||||
$aParams = array('class_list' => $aClassList);
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
|
||||
array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
@@ -3581,6 +3589,44 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
$sClass = get_class($this);
|
||||
if (MetaModel::HasLifecycle($sClass))
|
||||
{
|
||||
$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
|
||||
if (isset($this->m_aPreviousValuesForUpdatedAttributes[$sStateAttCode])) {
|
||||
$sPreviousState = $this->m_aPreviousValuesForUpdatedAttributes[$sStateAttCode];
|
||||
// Change state triggers...
|
||||
$aParams = array(
|
||||
'class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL),
|
||||
'previous_state' => $sPreviousState,
|
||||
'new_state' => $this->Get($sStateAttCode),
|
||||
);
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnStateLeave AS t WHERE t.target_class IN (:class_list) AND t.state=:previous_state'), array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnStateLeave $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivate($this->ToArgs('this'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $this);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnStateEnter AS t WHERE t.target_class IN (:class_list) AND t.state=:new_state'), array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnStateEnter $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivate($this->ToArgs('this'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $this);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activate any existing trigger
|
||||
// - TriggerOnObjectMention
|
||||
// Forgotten by the fix of N°3245
|
||||
@@ -3791,7 +3837,9 @@ abstract class DBObject implements iDisplay
|
||||
return;
|
||||
}
|
||||
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->OnDelete();
|
||||
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
|
||||
|
||||
// Activate any existing trigger
|
||||
$sClass = get_class($this);
|
||||
@@ -3899,7 +3947,9 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
|
||||
$this->FireEventAfterDelete();
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->AfterDelete();
|
||||
$oKPI->ComputeStatsForExtension($this, 'AfterDelete');
|
||||
|
||||
|
||||
$this->m_bIsInDB = false;
|
||||
@@ -3914,7 +3964,7 @@ abstract class DBObject implements iDisplay
|
||||
* First, checks if the object can be deleted regarding database integrity.
|
||||
* If the answer is yes, it performs any required cleanup (delete other objects or reset external keys) in addition to the object
|
||||
* deletion.
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param \DeletionPlan $oDeletionPlan Do not use: aims at dealing with recursion
|
||||
@@ -4272,36 +4322,6 @@ abstract class DBObject implements iDisplay
|
||||
$this->DBWrite();
|
||||
}
|
||||
|
||||
// Change state triggers...
|
||||
$aParams = array(
|
||||
'class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL),
|
||||
'previous_state' => $sPreviousState,
|
||||
'new_state' => $sNewState,
|
||||
);
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN (:class_list) AND t.state=:previous_state"), array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnStateLeave $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivate($this->ToArgs('this'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $this);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN (:class_list) AND t.state=:new_state"), array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnStateEnter $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivate($this->ToArgs('this'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $this);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->FireEvent(EVENT_DB_AFTER_APPLY_STIMULUS, $aEventData);
|
||||
}
|
||||
else
|
||||
@@ -4337,7 +4357,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*/
|
||||
*/
|
||||
public function Reset($sAttCode)
|
||||
{
|
||||
$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
|
||||
@@ -4349,7 +4369,7 @@ abstract class DBObject implements iDisplay
|
||||
* Suitable for use as a lifecycle action
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
*/
|
||||
public function Copy($sDestAttCode, $sSourceAttCode)
|
||||
{
|
||||
$oTypeValueToCopy = MetaModel::GetAttributeDef(get_class($this), $sSourceAttCode);
|
||||
@@ -4679,7 +4699,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this));
|
||||
}
|
||||
|
||||
|
||||
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
|
||||
if (!$oKeyAttDef instanceof AttributeExternalKey)
|
||||
{
|
||||
@@ -4697,14 +4717,14 @@ abstract class DBObject implements iDisplay
|
||||
$ret = $oRemoteObj->GetForTemplate($sRemoteAttCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
switch($sPlaceholderAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$ret = $this->GetKey();
|
||||
break;
|
||||
|
||||
|
||||
case 'name()':
|
||||
$ret = $this->GetName();
|
||||
break;
|
||||
@@ -4891,7 +4911,7 @@ abstract class DBObject implements iDisplay
|
||||
if ($oOwner)
|
||||
{
|
||||
$sLinkSetOwnerClass = get_class($oOwner);
|
||||
|
||||
|
||||
$oMyChangeOp = MetaModel::NewObject($sChangeOpClass);
|
||||
$oMyChangeOp->Set("objclass", $sLinkSetOwnerClass);
|
||||
$oMyChangeOp->Set("objkey", $iLinkSetOwnerId);
|
||||
@@ -4918,7 +4938,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
/** @var \AttributeLinkedSet $oLinkSet */
|
||||
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
|
||||
|
||||
|
||||
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
|
||||
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
|
||||
if ($oMyChangeOp)
|
||||
@@ -4988,7 +5008,7 @@ abstract class DBObject implements iDisplay
|
||||
// Keep track of link changes
|
||||
//
|
||||
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue;
|
||||
|
||||
|
||||
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
|
||||
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune');
|
||||
if ($oMyChangeOp)
|
||||
@@ -5137,7 +5157,7 @@ abstract class DBObject implements iDisplay
|
||||
$this->FireEventCheckToDelete($oDeletionPlan);
|
||||
$this->DoCheckToDelete($oDeletionPlan);
|
||||
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
|
||||
|
||||
|
||||
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
|
||||
|
||||
// Getting and setting time limit are not symmetric:
|
||||
@@ -5319,7 +5339,7 @@ abstract class DBObject implements iDisplay
|
||||
$aSynchroClasses[] = $sTarget;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach($aSynchroClasses as $sClass)
|
||||
{
|
||||
if ($this instanceof $sClass)
|
||||
|
||||
@@ -767,7 +767,10 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
|
||||
try
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->m_oSQLResult = CMDBSource::Query($sSQL);
|
||||
$sOQL = $this->GetPseudoOQL($this->m_oFilter, $this->GetRealSortOrder(), $this->m_iLimitCount, $this->m_iLimitStart, false);
|
||||
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
|
||||
} catch (MySQLException $e)
|
||||
{
|
||||
// 1116 = ER_TOO_MANY_TABLES
|
||||
@@ -847,8 +850,11 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
{
|
||||
if (is_null($this->m_iNumTotalDBRows))
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true);
|
||||
$resQuery = CMDBSource::Query($sSQL);
|
||||
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), 0, 0, true);
|
||||
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
|
||||
if (!$resQuery) return 0;
|
||||
|
||||
$aRow = CMDBSource::FetchArray($resQuery);
|
||||
@@ -859,6 +865,42 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ??
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBSearch $oFilter
|
||||
* @param array $aOrder
|
||||
* @param int $iLimitCount
|
||||
* @param int $iLimitStart
|
||||
* @param bool $bCount
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function GetPseudoOQL($oFilter, $aOrder, $iLimitCount, $iLimitStart, $bCount)
|
||||
{
|
||||
$sOQL = '';
|
||||
if ($bCount) {
|
||||
$sOQL .= 'COUNT ';
|
||||
}
|
||||
$sOQL .= $oFilter->ToOQL();
|
||||
|
||||
if ($iLimitCount > 0) {
|
||||
$sOQL .= ' LIMIT ';
|
||||
if ($iLimitStart > 0) {
|
||||
$sOQL .= "$iLimitStart, ";
|
||||
}
|
||||
$sOQL .= "$iLimitCount";
|
||||
}
|
||||
|
||||
if (count($aOrder) > 0) {
|
||||
$sOQL .= ' ORDER BY ';
|
||||
$aOrderBy = [];
|
||||
foreach ($aOrder as $sAttCode => $bAsc) {
|
||||
$aOrderBy[] = $sAttCode.' '.($bAsc ? 'ASC' : 'DESC');
|
||||
}
|
||||
$sOQL .= implode(', ', $aOrderBy);
|
||||
}
|
||||
return $sOQL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the count exceeds a given limit
|
||||
*
|
||||
@@ -875,8 +917,11 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
{
|
||||
if (is_null($this->m_iNumTotalDBRows))
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
|
||||
$resQuery = CMDBSource::Query($sSQL);
|
||||
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
|
||||
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
|
||||
if ($resQuery)
|
||||
{
|
||||
$aRow = CMDBSource::FetchArray($resQuery);
|
||||
@@ -887,7 +932,7 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
{
|
||||
$iCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$iCount = $this->m_iNumTotalDBRows;
|
||||
@@ -912,8 +957,11 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
{
|
||||
if (is_null($this->m_iNumTotalDBRows))
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
|
||||
$resQuery = CMDBSource::Query($sSQL);
|
||||
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
|
||||
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
|
||||
if ($resQuery)
|
||||
{
|
||||
$aRow = CMDBSource::FetchArray($resQuery);
|
||||
@@ -924,7 +972,7 @@ class DBObjectSet implements iDBObjectSetIterator
|
||||
{
|
||||
$iCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$iCount = $this->m_iNumTotalDBRows;
|
||||
|
||||
@@ -56,10 +56,11 @@ class Dict
|
||||
* @param $sLanguageCode
|
||||
*
|
||||
* @throws \DictExceptionUnknownLanguage
|
||||
* @since 3.0.4 3.1.1 3.2.0 Param $sLanguageCode becomes nullable
|
||||
*/
|
||||
public static function SetUserLanguage($sLanguageCode)
|
||||
public static function SetUserLanguage($sLanguageCode = null)
|
||||
{
|
||||
if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
|
||||
if (!is_null($sLanguageCode) && !array_key_exists($sLanguageCode, self::$m_aLanguages))
|
||||
{
|
||||
throw new DictExceptionUnknownLanguage($sLanguageCode);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
<?php
|
||||
// Copyright (C) 2010-2023 Combodo SARL
|
||||
//
|
||||
// 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
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
use Combodo\iTop\Core\Kpi\KpiLogData;
|
||||
use Combodo\iTop\Service\Module\ModuleService;
|
||||
|
||||
|
||||
/**
|
||||
* Measures operations duration, memory usage, etc. (and some other KPIs)
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
class ExecutionKPI
|
||||
@@ -30,6 +17,8 @@ class ExecutionKPI
|
||||
static protected $m_bEnabled_Memory = false;
|
||||
static protected $m_bBlameCaller = false;
|
||||
static protected $m_sAllowedUser = '*';
|
||||
static protected $m_bGenerateLegacyReport = true;
|
||||
static protected $m_fSlowQueries = 0;
|
||||
|
||||
static protected $m_aStats = []; // Recurrent operations
|
||||
static protected $m_aExecData = []; // One shot operations
|
||||
@@ -86,14 +75,39 @@ class ExecutionKPI
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function SetGenerateLegacyReport($bReportExtensionsOnly)
|
||||
{
|
||||
self::$m_bGenerateLegacyReport = $bReportExtensionsOnly;
|
||||
}
|
||||
|
||||
static public function SetSlowQueries($fSlowQueries)
|
||||
{
|
||||
self::$m_fSlowQueries = $fSlowQueries;
|
||||
}
|
||||
|
||||
static public function GetDescription()
|
||||
{
|
||||
$aFeatures = array();
|
||||
if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration';
|
||||
if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage';
|
||||
$sFeatures = implode(', ', $aFeatures);
|
||||
$sFeatures = 'Measures: '.implode(', ', $aFeatures);
|
||||
$sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
|
||||
return "KPI logging is active for $sFor. Measures: $sFeatures";
|
||||
$sSlowQueries = '';
|
||||
if (self::$m_fSlowQueries > 0) {
|
||||
$sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s";
|
||||
}
|
||||
|
||||
$aExtensions = [];
|
||||
/** @var \iKPILoggerExtension $oExtensionInstance */
|
||||
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
|
||||
$aExtensions[] = ModuleService::GetInstance()->GetModuleNameFromObject($oExtensionInstance);
|
||||
}
|
||||
$sExtensions = '';
|
||||
if (count($aExtensions) > 0) {
|
||||
$sExtensions = '. KPI Extensions: ['.implode(', ', $aExtensions).']';
|
||||
}
|
||||
|
||||
return "KPI logging is active for $sFor. $sFeatures$sSlowQueries$sExtensions";
|
||||
}
|
||||
|
||||
static public function ReportStats()
|
||||
@@ -101,7 +115,28 @@ class ExecutionKPI
|
||||
if (!self::IsEnabled()) return;
|
||||
|
||||
global $fItopStarted;
|
||||
global $iItopInitialMemory;
|
||||
$sExecId = microtime(); // id to differentiate the hrefs!
|
||||
$sRequest = $_SERVER['REQUEST_URI'].' ('.$_SERVER['REQUEST_METHOD'].')';
|
||||
if (isset($_POST['operation'])) {
|
||||
$sRequest .= ' operation: '.$_POST['operation'];
|
||||
}
|
||||
|
||||
$fStop = MyHelpers::getmicrotime();
|
||||
if (($fStop - $fItopStarted) > self::$m_fSlowQueries) {
|
||||
// Invoke extensions to log the KPI operation
|
||||
/** @var \iKPILoggerExtension $oExtensionInstance */
|
||||
$iCurrentMemory = self::memory_get_usage();
|
||||
$iPeakMemory = self::memory_get_peak_usage();
|
||||
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
|
||||
$oKPILogData = new KpiLogData(KpiLogData::TYPE_REQUEST, 'Page', $sRequest, $fItopStarted, $fStop, '', $iItopInitialMemory, $iCurrentMemory, $iPeakMemory);
|
||||
$oExtensionInstance->LogOperation($oKPILogData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!self::$m_bGenerateLegacyReport) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aBeginTimes = array();
|
||||
foreach (self::$m_aExecData as $aOpStats)
|
||||
@@ -114,9 +149,9 @@ class ExecutionKPI
|
||||
|
||||
$sHtml = "<hr/>";
|
||||
$sHtml .= "<div style=\"background-color: grey; padding: 10px;\">";
|
||||
$sHtml .= "<h3><a name=\"".md5($sExecId)."\">KPIs</a> - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")</h3>";
|
||||
$sHtml .= "<h3><a name=\"".md5($sExecId)."\">KPIs</a> - $sRequest</h3>";
|
||||
$oStarted = DateTime::createFromFormat('U.u', $fItopStarted);
|
||||
$sHtml .= "<p>".$oStarted->format('Y-m-d H:i:s.u')."</p>";
|
||||
$sHtml .= '<p>'.$oStarted->format('Y-m-d H:i:s.u').'</p>';
|
||||
$sHtml .= "<p>log_kpi_user_id: ".UserRights::GetUserId()."</p>";
|
||||
$sHtml .= "<div>";
|
||||
$sHtml .= "<table border=\"1\" style=\"$sTableStyle\">";
|
||||
@@ -257,7 +292,7 @@ class ExecutionKPI
|
||||
$sTotalInter = round($fTotalInter, 3);
|
||||
$sMinInter = round($fMinInter, 3);
|
||||
$sMaxInter = round($fMaxInter, 3);
|
||||
if (($fTotalInter >= $fSlowQueries))
|
||||
if (($fTotalInter >= self::$m_fSlowQueries))
|
||||
{
|
||||
if ($bDisplayHeader)
|
||||
{
|
||||
@@ -285,37 +320,19 @@ class ExecutionKPI
|
||||
self::Report($sHtml);
|
||||
}
|
||||
|
||||
public static function InitStats()
|
||||
{
|
||||
// Invoke extensions to initialize the KPI statistics
|
||||
/** @var \iKPILoggerExtension $oExtensionInstance */
|
||||
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
|
||||
$oExtensionInstance->InitStats();
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ResetCounters();
|
||||
self::Push($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack executions to remove children duration from stats
|
||||
*
|
||||
* @param \ExecutionKPI $oExecutionKPI
|
||||
*/
|
||||
private static function Push(ExecutionKPI $oExecutionKPI)
|
||||
{
|
||||
self::$m_aExecutionStack[] = $oExecutionKPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop current child and count its duration in its parent
|
||||
*
|
||||
* @param float|int $fChildDuration
|
||||
*/
|
||||
private static function Pop(float $fChildDuration = 0)
|
||||
{
|
||||
array_pop(self::$m_aExecutionStack);
|
||||
// Update the parent's children duration
|
||||
$oPrevExecutionKPI = end(self::$m_aExecutionStack);
|
||||
if ($oPrevExecutionKPI) {
|
||||
$oPrevExecutionKPI->m_fChildrenDuration += $fChildDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the duration since startup, and reset the counter for the next measure
|
||||
//
|
||||
@@ -323,9 +340,15 @@ class ExecutionKPI
|
||||
{
|
||||
global $fItopStarted;
|
||||
|
||||
if (!self::IsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aNewEntry = null;
|
||||
|
||||
if (self::$m_bEnabled_Duration) {
|
||||
$fStarted = $this->m_fStarted;
|
||||
$fStopped = $this->m_fStarted;
|
||||
if (self::$m_bEnabled_Duration) {
|
||||
$fStopped = MyHelpers::getmicrotime();
|
||||
$aNewEntry = array(
|
||||
'op' => $sOperationDesc,
|
||||
@@ -336,6 +359,9 @@ class ExecutionKPI
|
||||
$this->m_fStarted = $fStopped;
|
||||
}
|
||||
|
||||
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
|
||||
$iCurrentMemory = 0;
|
||||
$iPeakMemory = 0;
|
||||
if (self::$m_bEnabled_Memory)
|
||||
{
|
||||
$iCurrentMemory = self::memory_get_usage();
|
||||
@@ -345,40 +371,103 @@ class ExecutionKPI
|
||||
}
|
||||
$aNewEntry['mem_begin'] = $this->m_iInitialMemory;
|
||||
$aNewEntry['mem_end'] = $iCurrentMemory;
|
||||
if (function_exists('memory_get_peak_usage'))
|
||||
{
|
||||
$aNewEntry['mem_peak'] = memory_get_peak_usage();
|
||||
}
|
||||
$iPeakMemory = self::memory_get_peak_usage();
|
||||
$aNewEntry['mem_peak'] = $iPeakMemory;
|
||||
// Reset for the next operation (if the object is recycled)
|
||||
$this->m_iInitialMemory = $iCurrentMemory;
|
||||
}
|
||||
|
||||
if (!is_null($aNewEntry))
|
||||
if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) {
|
||||
// Invoke extensions to log the KPI operation
|
||||
/** @var \iKPILoggerExtension $oExtensionInstance */
|
||||
foreach(MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance)
|
||||
{
|
||||
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
|
||||
$oKPILogData = new KpiLogData(
|
||||
KpiLogData::TYPE_REPORT,
|
||||
'Step',
|
||||
$sOperationDesc,
|
||||
$fStarted,
|
||||
$fStopped,
|
||||
$sExtension,
|
||||
$iInitialMemory,
|
||||
$iCurrentMemory,
|
||||
$iPeakMemory);
|
||||
$oExtensionInstance->LogOperation($oKPILogData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($aNewEntry) && self::$m_bGenerateLegacyReport)
|
||||
{
|
||||
self::$m_aExecData[] = $aNewEntry;
|
||||
}
|
||||
$this->ResetCounters();
|
||||
}
|
||||
|
||||
public function ComputeStatsForExtension($object, $sMethod)
|
||||
{
|
||||
if (!self::IsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sSignature = ModuleService::GetInstance()->GetModuleMethodSignature($object, $sMethod);
|
||||
if (utils::StartsWith($sSignature, '[')) {
|
||||
$this->ComputeStats('Extension', $sSignature);
|
||||
}
|
||||
}
|
||||
|
||||
public function ComputeStats($sOperation, $sArguments)
|
||||
{
|
||||
if (!self::IsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fDuration = 0;
|
||||
if (self::$m_bEnabled_Duration) {
|
||||
$fStopped = MyHelpers::getmicrotime();
|
||||
$fDuration = $fStopped - $this->m_fStarted;
|
||||
$fSelfDuration = $fDuration - $this->m_fChildrenDuration;
|
||||
if (self::$m_bBlameCaller) {
|
||||
self::$m_aStats[$sOperation][$sArguments][] = array(
|
||||
'time' => $fSelfDuration,
|
||||
'callers' => MyHelpers::get_callstack(1),
|
||||
);
|
||||
} else {
|
||||
self::$m_aStats[$sOperation][$sArguments][] = array(
|
||||
'time' => $fSelfDuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
self::Pop($fDuration);
|
||||
$aCallstack = [];
|
||||
if (self::$m_bGenerateLegacyReport) {
|
||||
if (self::$m_bBlameCaller) {
|
||||
$aCallstack = MyHelpers::get_callstack(1);
|
||||
self::$m_aStats[$sOperation][$sArguments][] = [
|
||||
'time' => $fDuration,
|
||||
'callers' => $aCallstack,
|
||||
];
|
||||
} else {
|
||||
self::$m_aStats[$sOperation][$sArguments][] = [
|
||||
'time' => $fDuration
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
|
||||
$iCurrentMemory = 0;
|
||||
$iPeakMemory = 0;
|
||||
if (self::$m_bEnabled_Memory)
|
||||
{
|
||||
$iCurrentMemory = self::memory_get_usage();
|
||||
$iPeakMemory = self::memory_get_peak_usage();
|
||||
}
|
||||
|
||||
// Invoke extensions to log the KPI operation
|
||||
/** @var \iKPILoggerExtension $oExtensionInstance */
|
||||
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
|
||||
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
|
||||
$oKPILogData = new KpiLogData(
|
||||
KpiLogData::TYPE_STATS,
|
||||
$sOperation,
|
||||
$sArguments,
|
||||
$this->m_fStarted,
|
||||
$fStopped,
|
||||
$sExtension,
|
||||
$iInitialMemory,
|
||||
$iCurrentMemory,
|
||||
$iPeakMemory,
|
||||
$aCallstack);
|
||||
$oExtensionInstance->LogOperation($oKPILogData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function ResetCounters()
|
||||
@@ -408,35 +497,7 @@ class ExecutionKPI
|
||||
|
||||
static protected function memory_get_usage()
|
||||
{
|
||||
if (function_exists('memory_get_usage'))
|
||||
{
|
||||
return memory_get_usage(true);
|
||||
}
|
||||
|
||||
// Copied from the PHP manual
|
||||
//
|
||||
//If its Windows
|
||||
//Tested on Win XP Pro SP2. Should work on Win 2003 Server too
|
||||
//Doesn't work for 2000
|
||||
//If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642
|
||||
if (substr(PHP_OS,0,3) == 'WIN')
|
||||
{
|
||||
$output = array();
|
||||
exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output);
|
||||
|
||||
return preg_replace( '/[\D]/', '', $output[5] ) * 1024;
|
||||
}
|
||||
else
|
||||
{
|
||||
//We now assume the OS is UNIX
|
||||
//Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4
|
||||
//This should work on most UNIX systems
|
||||
$pid = getmypid();
|
||||
exec("ps -eo%mem,rss,pid | grep $pid", $output);
|
||||
$output = explode(" ", $output[0]);
|
||||
//rss is given in 1024 byte units
|
||||
return $output[1] * 1024;
|
||||
}
|
||||
return memory_get_usage(true);
|
||||
}
|
||||
|
||||
static public function memory_get_peak_usage($bRealUsage = false)
|
||||
|
||||
@@ -1138,7 +1138,7 @@ class DeprecatedCallsLog extends LogAPI
|
||||
parent::Enable($sTargetFile);
|
||||
|
||||
if (
|
||||
(false === defined(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME))
|
||||
(false === defined('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME'))
|
||||
&& static::IsLogLevelEnabledSafe(self::LEVEL_WARNING, self::ENUM_CHANNEL_PHP_LIBMETHOD)
|
||||
) {
|
||||
set_error_handler([static::class, 'DeprecatedNoticesErrorHandler'], E_DEPRECATED | E_USER_DEPRECATED);
|
||||
|
||||
@@ -1241,7 +1241,7 @@ abstract class MetaModel
|
||||
}
|
||||
$sTable = self::DBGetTable($sClass);
|
||||
|
||||
// Could be completed later with all the classes that are using a given table
|
||||
// Could be completed later with all the classes that are using a given table
|
||||
if (!array_key_exists($sTable, $aTables)) {
|
||||
$aTables[$sTable] = array();
|
||||
}
|
||||
@@ -3522,7 +3522,7 @@ abstract class MetaModel
|
||||
}
|
||||
|
||||
// Set the "host class" as soon as possible, since HierarchicalKeys use it for their 'target class' as well
|
||||
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
|
||||
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
|
||||
$oAtt->SetHostClass($sTargetClass);
|
||||
|
||||
// Some attributes could refer to a class
|
||||
@@ -3564,7 +3564,7 @@ abstract class MetaModel
|
||||
|
||||
self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt;
|
||||
self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass;
|
||||
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
|
||||
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3764,7 +3764,7 @@ abstract class MetaModel
|
||||
self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus;
|
||||
|
||||
// I wanted to simplify the syntax of the declaration of objects in the biz model
|
||||
// Therefore, the reference to the host class is set there
|
||||
// Therefore, the reference to the host class is set there
|
||||
$oStimulus->SetHostClass($sTargetClass);
|
||||
}
|
||||
|
||||
@@ -6298,6 +6298,13 @@ abstract class MetaModel
|
||||
*/
|
||||
public static function Startup($config, $bModelOnly = false, $bAllowCache = true, $bTraceSourceFiles = false, $sEnvironment = 'production')
|
||||
{
|
||||
// Startup on a new environment is not supported
|
||||
static $bStarted = false;
|
||||
if ($bStarted) {
|
||||
return;
|
||||
}
|
||||
$bStarted = true;
|
||||
|
||||
self::$m_sEnvironment = $sEnvironment;
|
||||
|
||||
try {
|
||||
@@ -6376,7 +6383,9 @@ abstract class MetaModel
|
||||
|
||||
ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration'));
|
||||
ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory'));
|
||||
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
|
||||
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
|
||||
ExecutionKPI::SetGenerateLegacyReport(self::$m_oConfig->Get('log_kpi_generate_legacy_report'));
|
||||
ExecutionKPI::SetSlowQueries(self::$m_oConfig->Get('log_kpi_slow_queries'));
|
||||
|
||||
self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write');
|
||||
self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys');
|
||||
@@ -6470,7 +6479,7 @@ abstract class MetaModel
|
||||
$aCache['m_aExtensionClassNames'] = self::$m_aExtensionClassNames;
|
||||
$aCache['m_Category2Class'] = self::$m_Category2Class;
|
||||
$aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass"
|
||||
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
|
||||
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
|
||||
$aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass")
|
||||
$aCache['m_aClassParams'] = self::$m_aClassParams; // array of ("classname" => array of class information)
|
||||
$aCache['m_aAttribDefs'] = self::$m_aAttribDefs; // array of ("classname" => array of attributes)
|
||||
@@ -6495,6 +6504,7 @@ abstract class MetaModel
|
||||
|
||||
CMDBSource::InitFromConfig(self::$m_oConfig);
|
||||
// Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone);
|
||||
ExecutionKPI::InitStats();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6526,6 +6536,19 @@ abstract class MetaModel
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Used for resetting the configuration during automated tests
|
||||
|
||||
* @param \Config $oConfiguration
|
||||
*
|
||||
* @return void
|
||||
* @since 3.0.4 3.1.1 3.2.0
|
||||
*/
|
||||
public static function SetConfig(Config $oConfiguration)
|
||||
{
|
||||
self::$m_oConfig = $oConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Config
|
||||
*/
|
||||
@@ -6807,25 +6830,21 @@ abstract class MetaModel
|
||||
* $bMustBeFound=false)
|
||||
* @throws CoreException if no result found and $bMustBeFound=true
|
||||
* @throws ArchivedObjectException if archive mode disabled and result is archived and $bMustBeFound=true
|
||||
* @throws \Exception
|
||||
*
|
||||
*/
|
||||
public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
|
||||
{
|
||||
$oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
|
||||
|
||||
if (empty($oObject))
|
||||
{
|
||||
if (empty($oObject)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!utils::IsArchiveMode() && $oObject->IsArchived())
|
||||
{
|
||||
if (!utils::IsArchiveMode() && $oObject->IsArchived()) {
|
||||
if ($bMustBeFound) {
|
||||
throw new ArchivedObjectException("The object $sClass::$iKey is archived");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $oObject;
|
||||
@@ -7567,20 +7586,6 @@ abstract class MetaModel
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5324: to ease reentrance checks when using events on links (to avoid reentering if main link object ongoing operation)
|
||||
*/
|
||||
public static function GetReentranceObjectByChildClass(string $sParentClass, $sKey)
|
||||
{
|
||||
foreach (self::EnumChildClasses($sParentClass, ENUM_CHILD_CLASSES_ALL, false) as $sChildClass){
|
||||
if (self::GetReentranceObject($sChildClass, $sKey)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBObject $oObject
|
||||
*
|
||||
@@ -7622,14 +7627,12 @@ abstract class MetaModel
|
||||
// Build the list of available extensions
|
||||
//
|
||||
$aInterfaces = [
|
||||
'iApplicationUIExtension',
|
||||
'iPreferencesExtension',
|
||||
'iApplicationObjectExtension',
|
||||
'iLoginFSMExtension',
|
||||
'iLoginUIExtension',
|
||||
'iLogoutExtension',
|
||||
'iQueryModifier',
|
||||
'iOnClassInitialization',
|
||||
'iLoginUIExtension',
|
||||
'iPreferencesExtension',
|
||||
'iApplicationUIExtension',
|
||||
'iApplicationObjectExtension',
|
||||
'iPopupMenuExtension',
|
||||
'iPageUIExtension',
|
||||
'iPageUIBlockExtension',
|
||||
@@ -7643,9 +7646,12 @@ abstract class MetaModel
|
||||
'iBackofficeDictEntriesExtension',
|
||||
'iBackofficeDictEntriesPrefixesExtension',
|
||||
'iPortalUIExtension',
|
||||
'iQueryModifier',
|
||||
'iOnClassInitialization',
|
||||
'iModuleExtension',
|
||||
'iKPILoggerExtension',
|
||||
'ModuleHandlerApiInterface',
|
||||
'iNewsroomProvider',
|
||||
'iModuleExtension',
|
||||
];
|
||||
foreach ($aInterfaces as $sInterface) {
|
||||
self::$m_aExtensionClassNames[$sInterface] = array();
|
||||
|
||||
@@ -257,7 +257,7 @@ class iTopMutex
|
||||
$this->hDBLink = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, false);
|
||||
|
||||
if (!$this->hDBLink) {
|
||||
throw new Exception("Could not connect to the DB server (host=$sServer, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
|
||||
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
|
||||
}
|
||||
|
||||
// Make sure that the server variable `wait_timeout` is at least 86400 seconds for this connection,
|
||||
|
||||
@@ -121,7 +121,9 @@ abstract class Trigger extends cmdbAbstractObject
|
||||
$oAction = MetaModel::GetObject('Action', $iActionId);
|
||||
if ($oAction->IsActive())
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$oAction->DoExecute($this, $aContextArgs);
|
||||
$oKPI->ComputeStatsForExtension($oAction, 'DoExecute');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,14 +761,25 @@ class UserRights
|
||||
protected static $m_aCacheContactPictureAbsUrl = [];
|
||||
/** @var UserRightsAddOnAPI $m_oAddOn */
|
||||
protected static $m_oAddOn;
|
||||
protected static $m_oUser;
|
||||
protected static $m_oRealUser;
|
||||
protected static $m_oUser = null;
|
||||
protected static $m_oRealUser = null;
|
||||
protected static $m_sSelfRegisterAddOn = null;
|
||||
protected static $m_aAdmins = array();
|
||||
protected static $m_aPortalUsers = array();
|
||||
/** @var array array('sName' => $sName, 'bSuccess' => $bSuccess); */
|
||||
private static $m_sLastLoginStatus = null;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @since 3.0.4 3.1.1 3.2.0
|
||||
*/
|
||||
protected static function ResetCurrentUserData()
|
||||
{
|
||||
self::$m_oUser = null;
|
||||
self::$m_oRealUser = null;
|
||||
self::$m_sLastLoginStatus = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sModuleName
|
||||
*
|
||||
@@ -787,8 +798,7 @@ class UserRights
|
||||
}
|
||||
self::$m_oAddOn = new $sModuleName;
|
||||
self::$m_oAddOn->Init();
|
||||
self::$m_oUser = null;
|
||||
self::$m_oRealUser = null;
|
||||
self::ResetCurrentUserData();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -855,6 +865,8 @@ class UserRights
|
||||
*/
|
||||
public static function Login($sLogin, $sAuthentication = 'any')
|
||||
{
|
||||
static::Logoff();
|
||||
|
||||
$oUser = self::FindUser($sLogin, $sAuthentication);
|
||||
if (is_null($oUser))
|
||||
{
|
||||
@@ -872,6 +884,17 @@ class UserRights
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @since 3.0.4 3.1.1 3.2.0
|
||||
*/
|
||||
public static function Logoff()
|
||||
{
|
||||
self::ResetCurrentUserData();
|
||||
Dict::SetUserLanguage(null);
|
||||
self::_ResetSessionCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sLogin Login of the user to check the credentials for
|
||||
* @param string $sPassword
|
||||
|
||||
2
css/backoffice/pages/_csv-import.scss
vendored
2
css/backoffice/pages/_csv-import.scss
vendored
@@ -51,4 +51,4 @@ tr.ibo-csv-import--row-added td {
|
||||
font-size: $ibo-csv-import--download-file--font-size;
|
||||
color: $ibo-csv-import--download-file--color;
|
||||
margin: $ibo-csv-import--download-file--margin;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,7 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
||||
|
||||
#welcome_popup{
|
||||
display: flex;
|
||||
}
|
||||
.ibo-welcome-popup--columns{
|
||||
display: flex;
|
||||
|
||||
}
|
||||
.ibo-welcome-popup--image{
|
||||
display: flex;
|
||||
@@ -46,39 +44,7 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ibo-welcome-popup--dialog {
|
||||
width: 60rem;
|
||||
}
|
||||
.ibo-welcome-popup--content {
|
||||
width: 100%;
|
||||
.ibo-welcome-popup--message {
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
}
|
||||
.ibo-welcome-popup--button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
position: absolute;
|
||||
bottom: 4.5rem;
|
||||
}
|
||||
}
|
||||
.ibo-welcome-popup--indicators {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 0;
|
||||
height: 3rem;
|
||||
.ibo-welcome-popup--indicator {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: $ibo-color-secondary-600;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ibo-welcome-popup--active {
|
||||
background-color: $ibo-color-information-600 !important;
|
||||
}
|
||||
.ibo-welcome-popup--text--options{
|
||||
position: absolute;
|
||||
bottom: $ibo-welcome-popup--text--options--bottom;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -235,13 +235,16 @@ class DBRestore extends DBBackup
|
||||
if (in_array($oFileInfo->getFilename(), $aStandardFiles)) {
|
||||
continue;
|
||||
}
|
||||
if (strncmp($oFileInfo->getPathname(), $sDataDir.'/production-modules', strlen($sDataDir.'/production-modules')) == 0) {
|
||||
// Normalize filenames to cope with Windows backslashes
|
||||
$sPath = str_replace('\\', '/', $oFileInfo->getPathname());
|
||||
$sRefPath = str_replace('\\', '/', $sDataDir.'/production-modules');
|
||||
if (strncmp($sPath, $sRefPath, strlen($sRefPath)) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$aExtraFiles[$oFileInfo->getPathname()] = APPROOT.substr($oFileInfo->getPathname(), strlen($sDataDir));
|
||||
}
|
||||
|
||||
|
||||
return $aExtraFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:FAQ/Attribute:category_id+' => '',
|
||||
'Class:FAQ/Attribute:category_name' => '类别名称',
|
||||
'Class:FAQ/Attribute:category_name+' => '',
|
||||
'Class:FAQ/Attribute:error_code' => '错误代码',
|
||||
'Class:FAQ/Attribute:error_code' => '错误编码',
|
||||
'Class:FAQ/Attribute:error_code+' => '',
|
||||
'Class:FAQ/Attribute:key_words' => '关键字',
|
||||
'Class:FAQ/Attribute:key_words+' => '',
|
||||
|
||||
@@ -66,11 +66,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:status+' => '',
|
||||
'Class:Incident/Attribute:status/Value:new' => '新建',
|
||||
'Class:Incident/Attribute:status/Value:new+' => '',
|
||||
'Class:Incident/Attribute:status/Value:escalated_tto' => '已升级响应时间',
|
||||
'Class:Incident/Attribute:status/Value:escalated_tto' => '已升级TTO',
|
||||
'Class:Incident/Attribute:status/Value:escalated_tto+' => '',
|
||||
'Class:Incident/Attribute:status/Value:assigned' => '已分配',
|
||||
'Class:Incident/Attribute:status/Value:assigned+' => '',
|
||||
'Class:Incident/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
|
||||
'Class:Incident/Attribute:status/Value:escalated_ttr' => '已升级TTR',
|
||||
'Class:Incident/Attribute:status/Value:escalated_ttr+' => '',
|
||||
'Class:Incident/Attribute:status/Value:waiting_for_approval' => '等待批准',
|
||||
'Class:Incident/Attribute:status/Value:waiting_for_approval+' => '',
|
||||
@@ -90,8 +90,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:impact/Value:3+' => '',
|
||||
'Class:Incident/Attribute:priority' => '优先级',
|
||||
'Class:Incident/Attribute:priority+' => '',
|
||||
'Class:Incident/Attribute:priority/Value:1' => '非常高',
|
||||
'Class:Incident/Attribute:priority/Value:1+' => '非常高',
|
||||
'Class:Incident/Attribute:priority/Value:1' => '紧急',
|
||||
'Class:Incident/Attribute:priority/Value:1+' => '紧急',
|
||||
'Class:Incident/Attribute:priority/Value:2' => '高',
|
||||
'Class:Incident/Attribute:priority/Value:2+' => '高',
|
||||
'Class:Incident/Attribute:priority/Value:3' => '中',
|
||||
@@ -100,8 +100,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:priority/Value:4+' => '低',
|
||||
'Class:Incident/Attribute:urgency' => '紧急度',
|
||||
'Class:Incident/Attribute:urgency+' => '',
|
||||
'Class:Incident/Attribute:urgency/Value:1' => '非常高',
|
||||
'Class:Incident/Attribute:urgency/Value:1+' => '非常高',
|
||||
'Class:Incident/Attribute:urgency/Value:1' => '紧急',
|
||||
'Class:Incident/Attribute:urgency/Value:1+' => '紧急',
|
||||
'Class:Incident/Attribute:urgency/Value:2' => '高',
|
||||
'Class:Incident/Attribute:urgency/Value:2+' => '高',
|
||||
'Class:Incident/Attribute:urgency/Value:3' => '中',
|
||||
@@ -136,7 +136,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:escalation_flag/Value:no+' => '否',
|
||||
'Class:Incident/Attribute:escalation_flag/Value:yes' => '是',
|
||||
'Class:Incident/Attribute:escalation_flag/Value:yes+' => '是',
|
||||
'Class:Incident/Attribute:escalation_reason' => '热门',
|
||||
'Class:Incident/Attribute:escalation_reason' => '升级原因',
|
||||
'Class:Incident/Attribute:escalation_reason+' => '',
|
||||
'Class:Incident/Attribute:assignment_date' => '分配日期',
|
||||
'Class:Incident/Attribute:assignment_date+' => '',
|
||||
@@ -146,21 +146,21 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:last_pending_date+' => '',
|
||||
'Class:Incident/Attribute:cumulatedpending' => '累计待定',
|
||||
'Class:Incident/Attribute:cumulatedpending+' => '',
|
||||
'Class:Incident/Attribute:tto' => '响应时间',
|
||||
'Class:Incident/Attribute:tto+' => '',
|
||||
'Class:Incident/Attribute:ttr' => '解决时间',
|
||||
'Class:Incident/Attribute:ttr+' => '',
|
||||
'Class:Incident/Attribute:tto_escalation_deadline' => '响应时间截止',
|
||||
'Class:Incident/Attribute:tto' => 'TTO',
|
||||
'Class:Incident/Attribute:tto+' => '响应时间',
|
||||
'Class:Incident/Attribute:ttr' => 'TTR',
|
||||
'Class:Incident/Attribute:ttr+' => '解决时限',
|
||||
'Class:Incident/Attribute:tto_escalation_deadline' => 'TTO截止日期',
|
||||
'Class:Incident/Attribute:tto_escalation_deadline+' => '',
|
||||
'Class:Incident/Attribute:sla_tto_passed' => '超过SLA响应时间',
|
||||
'Class:Incident/Attribute:sla_tto_passed' => 'SLA TTO 合格',
|
||||
'Class:Incident/Attribute:sla_tto_passed+' => '',
|
||||
'Class:Incident/Attribute:sla_tto_over' => 'SLA响应时间结束',
|
||||
'Class:Incident/Attribute:sla_tto_over' => 'SLA TTO 超时',
|
||||
'Class:Incident/Attribute:sla_tto_over+' => '',
|
||||
'Class:Incident/Attribute:ttr_escalation_deadline' => '解决时间截止',
|
||||
'Class:Incident/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
|
||||
'Class:Incident/Attribute:ttr_escalation_deadline+' => '',
|
||||
'Class:Incident/Attribute:sla_ttr_passed' => '超过SLA解决时间',
|
||||
'Class:Incident/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
|
||||
'Class:Incident/Attribute:sla_ttr_passed+' => '',
|
||||
'Class:Incident/Attribute:sla_ttr_over' => 'SLA解决时间结束',
|
||||
'Class:Incident/Attribute:sla_ttr_over' => 'SLA TTR 超时',
|
||||
'Class:Incident/Attribute:sla_ttr_over+' => '',
|
||||
'Class:Incident/Attribute:time_spent' => '耗时',
|
||||
'Class:Incident/Attribute:time_spent+' => '',
|
||||
@@ -182,7 +182,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Incident/Attribute:resolution_code/Value:training+' => '培训',
|
||||
'Class:Incident/Attribute:solution' => '解决方案',
|
||||
'Class:Incident/Attribute:solution+' => '',
|
||||
'Class:Incident/Attribute:pending_reason' => '待定的原因',
|
||||
'Class:Incident/Attribute:pending_reason' => '待定原因',
|
||||
'Class:Incident/Attribute:pending_reason+' => '',
|
||||
'Class:Incident/Attribute:parent_incident_id' => '父级事件',
|
||||
'Class:Incident/Attribute:parent_incident_id+' => '',
|
||||
|
||||
@@ -66,7 +66,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:KnownError/Attribute:workaround+' => '',
|
||||
'Class:KnownError/Attribute:solution' => '解决方案',
|
||||
'Class:KnownError/Attribute:solution+' => '',
|
||||
'Class:KnownError/Attribute:error_code' => '错误代码',
|
||||
'Class:KnownError/Attribute:error_code' => '错误编码',
|
||||
'Class:KnownError/Attribute:error_code+' => '',
|
||||
'Class:KnownError/Attribute:domain' => '类型',
|
||||
'Class:KnownError/Attribute:domain+' => '',
|
||||
|
||||
@@ -230,6 +230,7 @@ HTML
|
||||
$this->Set('refresh_token', $oAccessToken->getRefreshToken());
|
||||
}
|
||||
$this->Set('status', 'active');
|
||||
$this->AllowWrite();
|
||||
$this->DBUpdate();
|
||||
}
|
||||
]]></code>
|
||||
|
||||
@@ -11,6 +11,7 @@ use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
|
||||
use Dict;
|
||||
use IssueLog;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
use WebPage;
|
||||
@@ -65,13 +66,15 @@ class AjaxOauthClientController extends Controller
|
||||
}
|
||||
if (isset($aQuery['code'])) {
|
||||
$sCode = $aQuery['code'];
|
||||
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
|
||||
|
||||
$oOAuthClient->SetAccessToken($oAccessToken);
|
||||
|
||||
|
||||
|
||||
$aResult['status'] = 'success';
|
||||
try {
|
||||
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
|
||||
$oOAuthClient->SetAccessToken($oAccessToken);
|
||||
$aResult['status'] = 'success';
|
||||
}
|
||||
catch (IdentityProviderException $e) {
|
||||
$aResult['status'] = 'error';
|
||||
$aResult['error_description'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$aResult['status'] = 'error';
|
||||
|
||||
@@ -103,8 +103,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Problem/Attribute:impact/Value:3+' => '',
|
||||
'Class:Problem/Attribute:urgency' => '紧急度',
|
||||
'Class:Problem/Attribute:urgency+' => '',
|
||||
'Class:Problem/Attribute:urgency/Value:1' => '非常高',
|
||||
'Class:Problem/Attribute:urgency/Value:1+' => '非常高',
|
||||
'Class:Problem/Attribute:urgency/Value:1' => '紧急',
|
||||
'Class:Problem/Attribute:urgency/Value:1+' => '紧急',
|
||||
'Class:Problem/Attribute:urgency/Value:2' => '高',
|
||||
'Class:Problem/Attribute:urgency/Value:2+' => '高',
|
||||
'Class:Problem/Attribute:urgency/Value:3' => '中',
|
||||
@@ -113,8 +113,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:Problem/Attribute:urgency/Value:4+' => '低',
|
||||
'Class:Problem/Attribute:priority' => '优先级',
|
||||
'Class:Problem/Attribute:priority+' => '',
|
||||
'Class:Problem/Attribute:priority/Value:1' => '非常高',
|
||||
'Class:Problem/Attribute:priority/Value:1+' => '非常高',
|
||||
'Class:Problem/Attribute:priority/Value:1' => '紧急',
|
||||
'Class:Problem/Attribute:priority/Value:1+' => '紧急',
|
||||
'Class:Problem/Attribute:priority/Value:2' => '高',
|
||||
'Class:Problem/Attribute:priority/Value:2+' => '高',
|
||||
'Class:Problem/Attribute:priority/Value:3' => '中',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design version="3.1">
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.1">
|
||||
<classes/>
|
||||
<user_rights>
|
||||
<groups>
|
||||
@@ -544,19 +544,5 @@
|
||||
<groups/>
|
||||
</profile>
|
||||
</profiles>
|
||||
<dictionaries>
|
||||
<dictionary id="EN US">
|
||||
<entries>
|
||||
<entry id="Class:User/NonStandaloneProfileWarning" _delta="define">User profile %1$s cannot be standalone. You should add
|
||||
other profiles otherwise you may encounter access issue with this user.</entry>
|
||||
</entries>
|
||||
</dictionary>
|
||||
<dictionary id="FR FR">
|
||||
<entries>
|
||||
<entry id="Class:User/NonStandaloneProfileWarning" _delta="define">Le profil %1$s ne peut être seul. Sans le rajout d'autres
|
||||
profiles, l'utilisateur peut rencontrer des problèmes dans iTop.</entry>
|
||||
</entries>
|
||||
</dictionary>
|
||||
</dictionaries>
|
||||
</user_rights>
|
||||
</itop_design>
|
||||
|
||||
@@ -36,7 +36,6 @@ SetupWebPage::AddModule(
|
||||
// Components
|
||||
//
|
||||
'datamodel' => array(
|
||||
'src/UserProfilesEventListener.php'
|
||||
),
|
||||
'webservice' => array(
|
||||
//'webservices.itop-profiles-itil.php',
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\ItilProfiles;
|
||||
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Service\Events\iEventServiceSetup;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use LogChannels;
|
||||
|
||||
define('POWER_USER_PORTAL_PROFILE_NAME', 'Portal power user');
|
||||
|
||||
/**
|
||||
* Class UserProfilesEventListener
|
||||
*
|
||||
* @package Combodo\iTop\Core\EventListener
|
||||
* @since 3.1 N°5324 - Avoid to have users with non-standalone power portal profile only
|
||||
*
|
||||
*/
|
||||
class UserProfilesEventListener implements iEventServiceSetup
|
||||
{
|
||||
const USERPROFILE_REPAIR_ITOP_PARAM_NAME = 'security.single_profile_completion';
|
||||
private $bIsRepairmentEnabled = false;
|
||||
|
||||
//map: non standalone profile name => repairing profile id
|
||||
private $aNonStandaloneProfilesMap = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function RegisterEventsAndListeners()
|
||||
{
|
||||
$this->Init();
|
||||
|
||||
if (false === $this->bIsRepairmentEnabled){
|
||||
return;
|
||||
}
|
||||
|
||||
$aEventSource = [\User::class, \UserExternal::class, \UserInternal::class];
|
||||
EventService::RegisterListener(
|
||||
EVENT_DB_BEFORE_WRITE,
|
||||
[$this, 'OnUserEdition'],
|
||||
$aEventSource
|
||||
);
|
||||
|
||||
EventService::RegisterListener(
|
||||
EVENT_DB_BEFORE_WRITE,
|
||||
[ $this, 'OnUserProfileEdition' ],
|
||||
[ \URP_UserProfile::class ],
|
||||
[],
|
||||
null
|
||||
);
|
||||
|
||||
EventService::RegisterListener(
|
||||
EVENT_DB_CHECK_TO_DELETE,
|
||||
[ $this, 'OnUserProfileLinkDeletion' ],
|
||||
[ \URP_UserProfile::class ],
|
||||
[],
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public function IsRepairmentEnabled() : bool
|
||||
{
|
||||
return $this->bIsRepairmentEnabled;
|
||||
}
|
||||
|
||||
|
||||
public function OnUserEdition(EventData $oEventData): void {
|
||||
/** @var \User $oObject */
|
||||
$oUser = $oEventData->Get('object');
|
||||
|
||||
try {
|
||||
$this->ValidateThenRepairOrWarn($oUser);
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error('Exception occurred on RepairProfiles', LogChannels::DM_CRUD, [
|
||||
'user_class' => get_class($oUser),
|
||||
'user_id' => $oUser->GetKey(),
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception_stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
if ($e instanceof \CoreCannotSaveObjectException){
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function OnUserProfileEdition(EventData $oEventData): void {
|
||||
$oURP_UserProfile = $oEventData->Get('object');
|
||||
|
||||
try {
|
||||
$iUserId = $oURP_UserProfile->Get('userid');
|
||||
$oUser = \MetaModel::GetReentranceObjectByChildClass(\User::class, $iUserId);
|
||||
if (false !== $oUser){
|
||||
//user edition: handled by other event
|
||||
return;
|
||||
}
|
||||
|
||||
$oUser = \MetaModel::GetObject(\User::class, $iUserId);
|
||||
$aChanges = $oURP_UserProfile->ListChanges();
|
||||
if (array_key_exists('userid', $aChanges)) {
|
||||
$iUserId = $oURP_UserProfile->GetOriginal('userid');
|
||||
$oPreviousUser = \MetaModel::GetObject(\User::class, $iUserId);
|
||||
|
||||
$oProfileLinkSet = $oPreviousUser->Get('profile_list');
|
||||
$oProfileLinkSet->Rewind();
|
||||
$iCount = 0;
|
||||
while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
|
||||
if ($oCurrentURP_UserProfile->Get('userid') !== $oCurrentURP_UserProfile->GetOriginal('userid')) {
|
||||
$sRemovedProfileId = $oCurrentURP_UserProfile->GetOriginal('profileid');
|
||||
continue;
|
||||
}
|
||||
|
||||
$iCount++;
|
||||
if ($iCount > 1){
|
||||
//more than one profile: no repairment needed
|
||||
return;
|
||||
}
|
||||
$sSingleProfileName = $oCurrentURP_UserProfile->Get('profile');
|
||||
}
|
||||
$this->RepairProfileChangesOrWarn($oPreviousUser, $sSingleProfileName, $oURP_UserProfile, $sRemovedProfileId);
|
||||
} else if (array_key_exists('profileid', $aChanges)){
|
||||
$oCurrentUserProfileSet = $oUser->Get('profile_list');
|
||||
if ($oCurrentUserProfileSet->Count() === 1){
|
||||
$oProfile = $oCurrentUserProfileSet->Fetch();
|
||||
|
||||
$this->RepairProfileChangesOrWarn($oUser, $oProfile->Get('profile'), $oURP_UserProfile, $oProfile->GetOriginal("profileid"));
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error('OnUserProfileEdition Exception', LogChannels::DM_CRUD, [
|
||||
'user_id' => $iUserId,
|
||||
'lnk_id' => $oURP_UserProfile->GetKey(),
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception_stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
if ($e instanceof \CoreCannotSaveObjectException){
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function OnUserProfileLinkDeletion(EventData $oEventData): void {
|
||||
$oURP_UserProfile = $oEventData->Get('object');
|
||||
|
||||
try {
|
||||
$iUserId = $oURP_UserProfile->Get('userid');
|
||||
$oUser = \MetaModel::GetReentranceObjectByChildClass(\User::class, $iUserId);
|
||||
if (false !== $oUser){
|
||||
//user edition: handled by other event
|
||||
return;
|
||||
}
|
||||
|
||||
$oUser = \MetaModel::GetObject(\User::class, $iUserId);
|
||||
|
||||
/** @var \DeletionPlan $oDeletionPlan */
|
||||
$oDeletionPlan = $oEventData->Get('deletion_plan');
|
||||
$aDeletedURP_UserProfiles = [];
|
||||
if (! is_null($oDeletionPlan)){
|
||||
$aListDeletes = $oDeletionPlan->ListDeletes();
|
||||
if (array_key_exists(\URP_UserProfile::class, $aListDeletes)) {
|
||||
foreach ($aListDeletes[\URP_UserProfile::class] as $iId => $aDeletes) {
|
||||
$aDeletedURP_UserProfiles []= $iId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$oProfileLinkSet = $oUser->Get('profile_list');
|
||||
$oProfileLinkSet->Rewind();
|
||||
$iCount = 0;
|
||||
while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
|
||||
if (in_array($oCurrentURP_UserProfile->GetKey(), $aDeletedURP_UserProfiles)) {
|
||||
continue;
|
||||
}
|
||||
$iCount++;
|
||||
if ($iCount > 1){
|
||||
//more than one profile: no repairment needed
|
||||
return;
|
||||
}
|
||||
$sSingleProfileName = $oCurrentURP_UserProfile->Get('profile');
|
||||
}
|
||||
|
||||
$this->RepairProfileChangesOrWarn($oUser, $sSingleProfileName, $oURP_UserProfile, $oURP_UserProfile->Get('profileid'), true);
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error('OnUserProfileLinkDeletion Exception', LogChannels::DM_CRUD, [
|
||||
'user_id' => $iUserId,
|
||||
'profile_id' => $oURP_UserProfile->Get('profileid'),
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception_stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $aPortalDispatcherData: passed only for testing purpose
|
||||
*
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function Init($aPortalDispatcherData=null) : void {
|
||||
if (is_null($aPortalDispatcherData)){
|
||||
$aPortalDispatcherData = \PortalDispatcherData::GetData();
|
||||
}
|
||||
|
||||
$aNonStandaloneProfiles = \utils::GetConfig()->Get(self::USERPROFILE_REPAIR_ITOP_PARAM_NAME, null);
|
||||
|
||||
//When there are several customized portals on an itop, choosing a specific profile means choosing which portal user will access
|
||||
//In that case, itop administrator has to specify it via itop configuration. we dont use default profiles repairment otherwise
|
||||
if (is_null($aNonStandaloneProfiles)){
|
||||
if (count($aPortalDispatcherData) > 2){
|
||||
$this->bIsRepairmentEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$aPortalNames = array_keys($aPortalDispatcherData);
|
||||
sort($aPortalNames);
|
||||
if ($aPortalNames !== ['backoffice', 'itop-portal']){
|
||||
$this->bIsRepairmentEnabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($aNonStandaloneProfiles)){
|
||||
//default configuration in the case there are no customized portals
|
||||
$aNonStandaloneProfiles = [ POWER_USER_PORTAL_PROFILE_NAME => PORTAL_PROFILE_NAME ];
|
||||
}
|
||||
|
||||
if (! is_array($aNonStandaloneProfiles)){
|
||||
\IssueLog::Error(sprintf("%s is badly configured. it should be an array.", self::USERPROFILE_REPAIR_ITOP_PARAM_NAME), null, [self::USERPROFILE_REPAIR_ITOP_PARAM_NAME => $aNonStandaloneProfiles]);
|
||||
$this->bIsRepairmentEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($aNonStandaloneProfiles)){
|
||||
//Feature specifically disabled in itop configuration
|
||||
$this->bIsRepairmentEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$this->FetchRepairingProfileIds($aNonStandaloneProfiles);
|
||||
}
|
||||
|
||||
public function FetchRepairingProfileIds(array $aNonStandaloneProfiles) : void {
|
||||
$aProfiles = [];
|
||||
try {
|
||||
$aProfilesToSearch = array_unique(array_values($aNonStandaloneProfiles));
|
||||
if(($iIndex = array_search(null, $aProfilesToSearch)) !== false) {
|
||||
unset($aProfilesToSearch[$iIndex]);
|
||||
}
|
||||
|
||||
if (1 === count($aProfilesToSearch)){
|
||||
$sInCondition = sprintf('"%s"', array_pop($aProfilesToSearch));
|
||||
} else {
|
||||
$sInCondition = sprintf('"%s"', implode('","', $aProfilesToSearch));
|
||||
}
|
||||
|
||||
$sOql = "SELECT URP_Profiles WHERE name IN ($sInCondition)";
|
||||
$oSearch = \DBSearch::FromOQL($sOql);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
while(($oProfile = $oSet->Fetch()) != null) {
|
||||
$sProfileName = $oProfile->Get('name');
|
||||
$aProfiles[$sProfileName] = $oProfile->GetKey();
|
||||
}
|
||||
|
||||
$this->aNonStandaloneProfilesMap = [];
|
||||
foreach ($aNonStandaloneProfiles as $sNonStandaloneProfileName => $sRepairProfileName) {
|
||||
if (is_null($sRepairProfileName)) {
|
||||
$this->aNonStandaloneProfilesMap[$sNonStandaloneProfileName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! array_key_exists($sRepairProfileName, $aProfiles)) {
|
||||
throw new \Exception(sprintf("%s is badly configured. profile $sRepairProfileName does not exist.", self::USERPROFILE_REPAIR_ITOP_PARAM_NAME));
|
||||
}
|
||||
|
||||
$this->aNonStandaloneProfilesMap[$sNonStandaloneProfileName] = $aProfiles[$sRepairProfileName];
|
||||
}
|
||||
|
||||
$this->bIsRepairmentEnabled = true;
|
||||
} catch (\Exception $e) {
|
||||
IssueLog::Error('Exception when searching user portal profile', LogChannels::DM_CRUD, [
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception_stacktrace' => $e->getTraceAsString(),
|
||||
'aProfiles' => $aProfiles,
|
||||
'aNonStandaloneProfiles' => $aNonStandaloneProfiles,
|
||||
]);
|
||||
$this->bIsRepairmentEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function ValidateThenRepairOrWarn(\User $oUser) : void
|
||||
{
|
||||
$oCurrentUserProfileSet = $oUser->Get('profile_list');
|
||||
if ($oCurrentUserProfileSet->Count() === 1){
|
||||
$oProfile = $oCurrentUserProfileSet->Fetch();
|
||||
|
||||
$this->RepairUserChangesOrWarn($oUser, $oProfile->Get('profile'));
|
||||
}
|
||||
}
|
||||
|
||||
public function RepairUserChangesOrWarn(\User $oUser, string $sSingleProfileName) : void {
|
||||
if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
|
||||
$sRepairingProfileId = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
|
||||
if (is_null($sRepairingProfileId)){
|
||||
//Notify current user via session messages that there will be an issue
|
||||
//Without preventing from commiting
|
||||
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName);
|
||||
//$oUser::SetSessionMessage(get_class($oUser), $oUser->GetKey(), 1, $sMessage, 'WARNING', 1);
|
||||
throw new \CoreCannotSaveObjectException(array('issues' => [$sMessage], 'class' => get_class($oUser), 'id' => $oUser->GetKey()));
|
||||
} else {
|
||||
//Completing profiles profiles by adding repairing one : by default portal user to a power portal user
|
||||
$oUserProfile = new \URP_UserProfile();
|
||||
$oUserProfile->Set('profileid', $sRepairingProfileId);
|
||||
$oCurrentUserProfileSet = $oUser->Get('profile_list');
|
||||
$oCurrentUserProfileSet->AddItem($oUserProfile);
|
||||
$oUser->Set('profile_list', $oCurrentUserProfileSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function RepairProfileChangesOrWarn(\User $oUser, string $sSingleProfileName, \URP_UserProfile $oURP_UserProfile, string $sRemovedProfileId, $bIsRemoval=false) : void {
|
||||
if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
|
||||
$sRepairingProfileId = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
|
||||
if (is_null($sRepairingProfileId)
|
||||
|| ($sRepairingProfileId === $sRemovedProfileId) //cannot repair by readding same remove profile as it will raise uniqueness rule
|
||||
){
|
||||
//Notify current user via session messages that there will be an issue
|
||||
//Without preventing from commiting
|
||||
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName);
|
||||
//$oURP_UserProfile::SetSessionMessage(get_class($oURP_UserProfile), $oURP_UserProfile->GetKey(), 1, $sMessage, 'WARNING', 1);
|
||||
if ($bIsRemoval){
|
||||
$oURP_UserProfile->AddDeleteIssue($sMessage);
|
||||
} else {
|
||||
throw new \CoreCannotSaveObjectException(array('issues' => [$sMessage], 'class' => get_class($oURP_UserProfile), 'id' => $oURP_UserProfile->GetKey()));
|
||||
}
|
||||
} else {
|
||||
//Completing profiles profiles by adding repairing one : by default portal user to a power portal user
|
||||
$oUserProfile = new \URP_UserProfile();
|
||||
$oUserProfile->Set('profileid', $sRepairingProfileId);
|
||||
$oCurrentUserProfileSet = $oUser->Get('profile_list');
|
||||
$oCurrentUserProfileSet->AddItem($oUserProfile);
|
||||
$oUser->Set('profile_list', $oCurrentUserProfileSet);
|
||||
$oUser->DBWrite();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:status+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:new' => '新建',
|
||||
'Class:UserRequest/Attribute:status/Value:new+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级响应时间',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级TTO',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:assigned' => '已分配',
|
||||
'Class:UserRequest/Attribute:status/Value:assigned+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级TTR',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:waiting_for_approval' => '等待批准',
|
||||
'Class:UserRequest/Attribute:status/Value:waiting_for_approval+' => '',
|
||||
@@ -90,8 +90,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:impact/Value:3+' => '',
|
||||
'Class:UserRequest/Attribute:priority' => '优先级',
|
||||
'Class:UserRequest/Attribute:priority+' => '',
|
||||
'Class:UserRequest/Attribute:priority/Value:1' => '非常高',
|
||||
'Class:UserRequest/Attribute:priority/Value:1+' => '非常高',
|
||||
'Class:UserRequest/Attribute:priority/Value:1' => '紧急',
|
||||
'Class:UserRequest/Attribute:priority/Value:1+' => '紧急',
|
||||
'Class:UserRequest/Attribute:priority/Value:2' => '高',
|
||||
'Class:UserRequest/Attribute:priority/Value:2+' => '高',
|
||||
'Class:UserRequest/Attribute:priority/Value:3' => '中',
|
||||
@@ -100,8 +100,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:priority/Value:4+' => '低',
|
||||
'Class:UserRequest/Attribute:urgency' => '紧急度',
|
||||
'Class:UserRequest/Attribute:urgency+' => '',
|
||||
'Class:UserRequest/Attribute:urgency/Value:1' => '非常高',
|
||||
'Class:UserRequest/Attribute:urgency/Value:1+' => '非常高',
|
||||
'Class:UserRequest/Attribute:urgency/Value:1' => '紧急',
|
||||
'Class:UserRequest/Attribute:urgency/Value:1+' => '紧急',
|
||||
'Class:UserRequest/Attribute:urgency/Value:2' => '高',
|
||||
'Class:UserRequest/Attribute:urgency/Value:2+' => '高',
|
||||
'Class:UserRequest/Attribute:urgency/Value:3' => '中',
|
||||
@@ -134,7 +134,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:servicesubcategory_id+' => '',
|
||||
'Class:UserRequest/Attribute:servicesubcategory_name' => '子服务名称',
|
||||
'Class:UserRequest/Attribute:servicesubcategory_name+' => '',
|
||||
'Class:UserRequest/Attribute:escalation_flag' => '是否升级',
|
||||
'Class:UserRequest/Attribute:escalation_flag' => '升级标签',
|
||||
'Class:UserRequest/Attribute:escalation_flag+' => '',
|
||||
'Class:UserRequest/Attribute:escalation_flag/Value:no' => '否',
|
||||
'Class:UserRequest/Attribute:escalation_flag/Value:no+' => '否',
|
||||
@@ -150,30 +150,30 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:last_pending_date+' => '',
|
||||
'Class:UserRequest/Attribute:cumulatedpending' => '累计待定',
|
||||
'Class:UserRequest/Attribute:cumulatedpending+' => '',
|
||||
'Class:UserRequest/Attribute:tto' => '响应时间',
|
||||
'Class:UserRequest/Attribute:tto' => 'TTO',
|
||||
'Class:UserRequest/Attribute:tto+' => '',
|
||||
'Class:UserRequest/Attribute:ttr' => '解决时间',
|
||||
'Class:UserRequest/Attribute:ttr' => 'TTR',
|
||||
'Class:UserRequest/Attribute:ttr+' => '',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline' => '响应时间截止',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline' => 'TTO截止日期',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline+' => '',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed' => '超过SLA响应时间',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed' => 'SLA TTO 合格',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed+' => '',
|
||||
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA响应时间超过',
|
||||
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA TTO 超时',
|
||||
'Class:UserRequest/Attribute:sla_tto_over+' => '',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline' => '解决时间截止',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline+' => '',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed' => '超过SLA解决时间',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed+' => '',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA解决时间超过',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA TTR 超时',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over+' => '',
|
||||
'Class:UserRequest/Attribute:time_spent' => '耗时',
|
||||
'Class:UserRequest/Attribute:time_spent+' => '',
|
||||
'Class:UserRequest/Attribute:resolution_code' => '解决代码',
|
||||
'Class:UserRequest/Attribute:resolution_code' => '解决编码',
|
||||
'Class:UserRequest/Attribute:resolution_code+' => '',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:assistance' => '帮助',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:assistance+' => '帮助',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed' => '缺陷修复',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed+' => '缺陷修复',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed' => 'bug修复',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:bug fixed+' => 'bug修复',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:hardware repair' => '硬件维修',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:hardware repair+' => '硬件维修',
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:other' => '其它',
|
||||
|
||||
@@ -62,11 +62,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:status+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:new' => '新建',
|
||||
'Class:UserRequest/Attribute:status/Value:new+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级响应时间',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto' => '已升级TTO',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_tto+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:assigned' => '已分配',
|
||||
'Class:UserRequest/Attribute:status/Value:assigned+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级解决时间',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr' => '已升级TTR',
|
||||
'Class:UserRequest/Attribute:status/Value:escalated_ttr+' => '',
|
||||
'Class:UserRequest/Attribute:status/Value:waiting_for_approval' => '等待批准',
|
||||
'Class:UserRequest/Attribute:status/Value:waiting_for_approval+' => '',
|
||||
@@ -156,21 +156,21 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:last_pending_date+' => '',
|
||||
'Class:UserRequest/Attribute:cumulatedpending' => '累计待定',
|
||||
'Class:UserRequest/Attribute:cumulatedpending+' => '',
|
||||
'Class:UserRequest/Attribute:tto' => '响应时间',
|
||||
'Class:UserRequest/Attribute:tto+' => '',
|
||||
'Class:UserRequest/Attribute:ttr' => '解决时间',
|
||||
'Class:UserRequest/Attribute:ttr+' => '',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline' => '响应时间期限',
|
||||
'Class:UserRequest/Attribute:tto' => 'TTO',
|
||||
'Class:UserRequest/Attribute:tto+' => '响应时间',
|
||||
'Class:UserRequest/Attribute:ttr' => 'TTR',
|
||||
'Class:UserRequest/Attribute:ttr+' => '解决时限',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline' => 'TTO截止日期',
|
||||
'Class:UserRequest/Attribute:tto_escalation_deadline+' => '',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed' => '超过SLA响应时间',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed' => 'SLA TTO 合格',
|
||||
'Class:UserRequest/Attribute:sla_tto_passed+' => '',
|
||||
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA响应时间结束',
|
||||
'Class:UserRequest/Attribute:sla_tto_over' => 'SLA TTO 超时',
|
||||
'Class:UserRequest/Attribute:sla_tto_over+' => '',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline' => '解决时间期限',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline' => 'TTR截止日期',
|
||||
'Class:UserRequest/Attribute:ttr_escalation_deadline+' => '',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed' => '超过SLA解决时间',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed' => 'SLA TTR 合格',
|
||||
'Class:UserRequest/Attribute:sla_ttr_passed+' => '',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA解决时间结束',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over' => 'SLA TTR 超时',
|
||||
'Class:UserRequest/Attribute:sla_ttr_over+' => '',
|
||||
'Class:UserRequest/Attribute:time_spent' => '耗时',
|
||||
'Class:UserRequest/Attribute:time_spent+' => '',
|
||||
@@ -192,7 +192,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:UserRequest/Attribute:resolution_code/Value:training+' => '培训',
|
||||
'Class:UserRequest/Attribute:solution' => '解决方案',
|
||||
'Class:UserRequest/Attribute:solution+' => '',
|
||||
'Class:UserRequest/Attribute:pending_reason' => '待定的原因',
|
||||
'Class:UserRequest/Attribute:pending_reason' => '待定原因',
|
||||
'Class:UserRequest/Attribute:pending_reason+' => '',
|
||||
'Class:UserRequest/Attribute:parent_request_id' => '父级需求',
|
||||
'Class:UserRequest/Attribute:parent_request_id+' => '',
|
||||
|
||||
@@ -389,8 +389,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:SLT/Attribute:name+' => '',
|
||||
'Class:SLT/Attribute:priority' => '优先级',
|
||||
'Class:SLT/Attribute:priority+' => '',
|
||||
'Class:SLT/Attribute:priority/Value:1' => '非常高',
|
||||
'Class:SLT/Attribute:priority/Value:1+' => '非常高',
|
||||
'Class:SLT/Attribute:priority/Value:1' => '紧急',
|
||||
'Class:SLT/Attribute:priority/Value:1+' => '紧急',
|
||||
'Class:SLT/Attribute:priority/Value:2' => '高',
|
||||
'Class:SLT/Attribute:priority/Value:2+' => '高',
|
||||
'Class:SLT/Attribute:priority/Value:3' => '中',
|
||||
@@ -403,15 +403,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:SLT/Attribute:request_type/Value:incident+' => '事件',
|
||||
'Class:SLT/Attribute:request_type/Value:service_request' => '服务需求',
|
||||
'Class:SLT/Attribute:request_type/Value:service_request+' => '服务需求',
|
||||
'Class:SLT/Attribute:metric' => '指标',
|
||||
'Class:SLT/Attribute:metric' => '衡量指标',
|
||||
'Class:SLT/Attribute:metric+' => '',
|
||||
'Class:SLT/Attribute:metric/Value:tto' => '响应时间',
|
||||
'Class:SLT/Attribute:metric/Value:tto' => 'TTO',
|
||||
'Class:SLT/Attribute:metric/Value:tto+' => '响应时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr' => '解决时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr' => 'TTR',
|
||||
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时限',
|
||||
'Class:SLT/Attribute:value' => '值',
|
||||
'Class:SLT/Attribute:value+' => '',
|
||||
'Class:SLT/Attribute:unit' => '单位',
|
||||
'Class:SLT/Attribute:unit' => '度量单位',
|
||||
'Class:SLT/Attribute:unit+' => '',
|
||||
'Class:SLT/Attribute:unit/Value:hours' => '小时',
|
||||
'Class:SLT/Attribute:unit/Value:hours+' => '小时',
|
||||
|
||||
@@ -362,8 +362,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:SLT/Attribute:name+' => '',
|
||||
'Class:SLT/Attribute:priority' => '优先级',
|
||||
'Class:SLT/Attribute:priority+' => '',
|
||||
'Class:SLT/Attribute:priority/Value:1' => '非常高',
|
||||
'Class:SLT/Attribute:priority/Value:1+' => '非常高',
|
||||
'Class:SLT/Attribute:priority/Value:1' => '紧急',
|
||||
'Class:SLT/Attribute:priority/Value:1+' => '紧急',
|
||||
'Class:SLT/Attribute:priority/Value:2' => '高',
|
||||
'Class:SLT/Attribute:priority/Value:2+' => '高',
|
||||
'Class:SLT/Attribute:priority/Value:3' => '中',
|
||||
@@ -376,15 +376,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:SLT/Attribute:request_type/Value:incident+' => '事件',
|
||||
'Class:SLT/Attribute:request_type/Value:service_request' => '服务需求',
|
||||
'Class:SLT/Attribute:request_type/Value:service_request+' => '服务需求',
|
||||
'Class:SLT/Attribute:metric' => '指标',
|
||||
'Class:SLT/Attribute:metric' => '衡量指标',
|
||||
'Class:SLT/Attribute:metric+' => '',
|
||||
'Class:SLT/Attribute:metric/Value:tto' => '响应时间',
|
||||
'Class:SLT/Attribute:metric/Value:tto' => 'TTO',
|
||||
'Class:SLT/Attribute:metric/Value:tto+' => '响应时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr' => '解决时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时间',
|
||||
'Class:SLT/Attribute:metric/Value:ttr' => 'TTR',
|
||||
'Class:SLT/Attribute:metric/Value:ttr+' => '解决时限',
|
||||
'Class:SLT/Attribute:value' => '值',
|
||||
'Class:SLT/Attribute:value+' => '',
|
||||
'Class:SLT/Attribute:unit' => '单位',
|
||||
'Class:SLT/Attribute:unit' => '度量单位',
|
||||
'Class:SLT/Attribute:unit+' => '',
|
||||
'Class:SLT/Attribute:unit/Value:hours' => '小时',
|
||||
'Class:SLT/Attribute:unit/Value:hours+' => '小时',
|
||||
|
||||
@@ -251,7 +251,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:DocumentNote' => '文档笔记',
|
||||
'Class:DocumentNote+' => '',
|
||||
'Class:DocumentNote/Attribute:text' => '文本',
|
||||
'Class:DocumentNote/Attribute:text' => '正文',
|
||||
'Class:DocumentNote/Attribute:text+' => '',
|
||||
));
|
||||
|
||||
|
||||
@@ -240,9 +240,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:cmdbAbstractObject/Method:ApplyStimulus+' => 'Apply the specified stimulus to the current object',
|
||||
'Class:cmdbAbstractObject/Method:ApplyStimulus/Param:1' => 'Stimulus code',
|
||||
'Class:cmdbAbstractObject/Method:ApplyStimulus/Param:1+' => 'A valid stimulus code for the current class',
|
||||
'Class:ResponseTicketTTO/Interface:iMetricComputer' => '响应时间',
|
||||
'Class:ResponseTicketTTO/Interface:iMetricComputer' => 'TTO',
|
||||
'Class:ResponseTicketTTO/Interface:iMetricComputer+' => 'SLT 的响应时间',
|
||||
'Class:ResponseTicketTTR/Interface:iMetricComputer' => '解决时间',
|
||||
'Class:ResponseTicketTTR/Interface:iMetricComputer+' => 'SLT 的解决时间',
|
||||
'Class:ResponseTicketTTR/Interface:iMetricComputer' => 'TTR',
|
||||
'Class:ResponseTicketTTR/Interface:iMetricComputer+' => 'SLT 的解决时限',
|
||||
));
|
||||
|
||||
|
||||
@@ -452,7 +452,6 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
We hope you’ll enjoy this version as much as we enjoyed imagining and creating it.</div>
|
||||
|
||||
<div>Customize your '.ITOP_APPLICATION.' preferences for a personalized experience.</div>',
|
||||
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, discard this message',
|
||||
'UI:WelcomeMenu:AllOpenRequests' => 'Open requests: %1$d',
|
||||
'UI:WelcomeMenu:MyCalls' => 'My requests',
|
||||
'UI:WelcomeMenu:OpenIncidents' => 'Open incidents: %1$d',
|
||||
|
||||
@@ -444,7 +444,6 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
|
||||
Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.</div>
|
||||
|
||||
<div>Configure las preferencias de '.ITOP_APPLICATION.' para una experiencia personalizada.</div>',
|
||||
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, descartar este mensaje',
|
||||
'UI:WelcomeMenu:AllOpenRequests' => 'Requerimientos Abiertos: %1$d',
|
||||
'UI:WelcomeMenu:MyCalls' => 'Mis Requerimientos',
|
||||
'UI:WelcomeMenu:OpenIncidents' => 'Incidentes Abiertos: %1$d',
|
||||
|
||||
@@ -433,7 +433,6 @@ Dict::Add('FR FR', 'French', 'Français', array(
|
||||
Nous espérons que vous aimerez cette version autant que nous avons eu du plaisir à l\'imaginer et à la créer.</div>
|
||||
|
||||
<div>Configurez vos préférences '.ITOP_APPLICATION.' pour une expérience personnalisée.</div>',
|
||||
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, supprimer ce message',
|
||||
'UI:WelcomeMenu:AllOpenRequests' => 'Requêtes en cours: %1$d',
|
||||
'UI:WelcomeMenu:MyCalls' => 'Mes appels support',
|
||||
'UI:WelcomeMenu:OpenIncidents' => 'Incidents en cours: %1$d',
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
// Display DataTable
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'UI:Datatables:Language:Processing' => '请稍候...',
|
||||
'UI:Datatables:Language:LengthMenu' => '_MENU_ 每页',
|
||||
'UI:Datatables:Language:LengthMenu' => '每页 _MENU_ 项',
|
||||
'UI:Datatables:Language:ZeroRecords' => '未找到相关结果',
|
||||
'UI:Datatables:Language:Info' => '_TOTAL_ 项',
|
||||
'UI:Datatables:Language:Info' => '共 _TOTAL_ 项',
|
||||
'UI:Datatables:Language:InfoEmpty' => '未找到相关信息',
|
||||
'UI:Datatables:Language:EmptyTable' => '表格中暂无数据',
|
||||
'UI:Datatables:Language:Error' => '运行查询时出错',
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
// Navigation menu
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'UI:Layout:NavigationMenu:CompanyLogo:AltText' => '公司logo',
|
||||
|
||||
@@ -20,44 +20,60 @@
|
||||
* 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', '简体中文', array(
|
||||
'Core:DeletedObjectLabel' => '%1s (已删除)',
|
||||
'Core:DeletedObjectTip' => '对象已被删除于 %1$s (%2$s)',
|
||||
|
||||
'Core:UnknownObjectLabel' => '对象找不到 (class: %1$s, id: %2$d)',
|
||||
'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.~~',
|
||||
|
||||
'Core:UniquenessDefaultError' => 'Uniqueness rule \'%1$s\' in error~~',
|
||||
'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s~~',
|
||||
'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s~~',
|
||||
|
||||
'Core:AttributeLinkedSet' => '对象数组',
|
||||
'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass~~',
|
||||
|
||||
'Core:AttributeLinkedSetDuplicatesFound' => 'Duplicates in the \'%1$s\' field : %2$s~~',
|
||||
|
||||
'Core:AttributeDashboard' => '仪表盘',
|
||||
'Core:AttributeDashboard+' => '',
|
||||
|
||||
'Core:AttributePhoneNumber' => '电话号码',
|
||||
'Core:AttributePhoneNumber+' => '',
|
||||
|
||||
'Core:AttributeObsolescenceDate' => '报废日期',
|
||||
'Core:AttributeObsolescenceDate+' => '',
|
||||
|
||||
'Core:AttributeTagSet' => '清单',
|
||||
'Core:AttributeTagSet+' => '',
|
||||
'Core:AttributeSet:placeholder' => '请点击这里添加',
|
||||
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)~~',
|
||||
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)~~',
|
||||
'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses' => '%1$s (%2$s from child classes)~~',
|
||||
|
||||
'Core:AttributeCaseLog' => '日志',
|
||||
'Core:AttributeCaseLog+' => '',
|
||||
|
||||
'Core:AttributeMetaEnum' => 'Computed enum~~',
|
||||
'Core:AttributeMetaEnum+' => '~~',
|
||||
|
||||
'Core:AttributeLinkedSetIndirect' => '对象数组(N-N)',
|
||||
'Core:AttributeLinkedSetIndirect+' => 'Any kind of objects [subclass] of the same class~~',
|
||||
|
||||
'Core:AttributeInteger' => '整数',
|
||||
'Core:AttributeInteger+' => '整数值(可以为负)',
|
||||
|
||||
'Core:AttributeDecimal' => '小数',
|
||||
'Core:AttributeDecimal+' => '小数(可以为负)',
|
||||
|
||||
'Core:AttributeBoolean' => '布尔',
|
||||
'Core:AttributeBoolean+' => '布尔',
|
||||
'Core:AttributeBoolean+' => '',
|
||||
'Core:AttributeBoolean/Value:null' => '',
|
||||
'Core:AttributeBoolean/Value:yes' => '是',
|
||||
'Core:AttributeBoolean/Value:no' => '否',
|
||||
|
||||
'Core:AttributeArchiveFlag' => '是否归档',
|
||||
'Core:AttributeArchiveFlag/Value:yes' => '是',
|
||||
'Core:AttributeArchiveFlag/Value:yes+' => '此对象仅在归档模式可见',
|
||||
@@ -66,6 +82,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Core:AttributeArchiveFlag/Label+' => '',
|
||||
'Core:AttributeArchiveDate/Label' => '归档日期',
|
||||
'Core:AttributeArchiveDate/Label+' => '',
|
||||
|
||||
'Core:AttributeObsolescenceFlag' => '是否废弃',
|
||||
'Core:AttributeObsolescenceFlag/Value:yes' => '是',
|
||||
'Core:AttributeObsolescenceFlag/Value:yes+' => 'This object is excluded from the impact analysis, and hidden from search results~~',
|
||||
@@ -74,38 +91,54 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes~~',
|
||||
'Core:AttributeObsolescenceDate/Label' => '废弃时间',
|
||||
'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete~~',
|
||||
|
||||
'Core:AttributeString' => '字符串',
|
||||
'Core:AttributeString+' => '字符串',
|
||||
|
||||
'Core:AttributeClass' => '类',
|
||||
'Core:AttributeClass+' => '类别',
|
||||
'Core:AttributeClass+' => '',
|
||||
|
||||
'Core:AttributeApplicationLanguage' => '用户语言',
|
||||
'Core:AttributeApplicationLanguage+' => '语言和国家地区(EN US)',
|
||||
|
||||
'Core:AttributeFinalClass' => '类 (auto)',
|
||||
'Core:AttributeFinalClass+' => 'Real class of the object (automatically created by the core)',
|
||||
|
||||
'Core:AttributePassword' => '密码',
|
||||
'Core:AttributePassword+' => '外部设备的密码',
|
||||
|
||||
'Core:AttributeEncryptedString' => '加密字符串',
|
||||
'Core:AttributeEncryptedString+' => 'String encrypted with a local key~~',
|
||||
'Core:AttributeEncryptUnknownLibrary' => '未知的加密库 (%1$s)',
|
||||
'Core:AttributeEncryptFailedToDecrypt' => '** 解密错误 **',
|
||||
|
||||
'Core:AttributeText' => '文本',
|
||||
'Core:AttributeText+' => '多行字符串',
|
||||
|
||||
'Core:AttributeHTML' => 'HTML',
|
||||
'Core:AttributeHTML+' => 'HTML字符串',
|
||||
|
||||
'Core:AttributeEmailAddress' => '邮箱地址',
|
||||
'Core:AttributeEmailAddress+' => '邮箱地址',
|
||||
|
||||
'Core:AttributeIPAddress' => 'IP地址',
|
||||
'Core:AttributeIPAddress+' => 'IP地址',
|
||||
|
||||
'Core:AttributeOQL' => 'OQL',
|
||||
'Core:AttributeOQL+' => 'Object Query Langage expression~~',
|
||||
|
||||
'Core:AttributeEnum' => 'Enum~~',
|
||||
'Core:AttributeEnum+' => 'List of predefined alphanumeric strings~~',
|
||||
|
||||
'Core:AttributeTemplateString' => '字符模板',
|
||||
'Core:AttributeTemplateString+' => '包含占位符的字符串',
|
||||
|
||||
'Core:AttributeTemplateText' => '文字模板',
|
||||
'Core:AttributeTemplateText+' => '包含占位符的文本',
|
||||
|
||||
'Core:AttributeTemplateHTML' => 'HTML模板',
|
||||
'Core:AttributeTemplateHTML+' => 'HTML containing placeholders~~',
|
||||
|
||||
'Core:AttributeDateTime' => '日期/时间',
|
||||
'Core:AttributeDateTime+' => 'Date and time (年-月-日 时:分:秒)',
|
||||
'Core:AttributeDateTime?SmartSearch' => '
|
||||
@@ -123,6 +156,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
<p>
|
||||
如果不写具体时间,则默认00:00:00
|
||||
</p>',
|
||||
|
||||
'Core:AttributeDate' => '日期',
|
||||
'Core:AttributeDate+' => '日期 (年-月-日)',
|
||||
'Core:AttributeDate?SmartSearch' => '
|
||||
@@ -137,30 +171,43 @@ Operators:<br/>
|
||||
<b><</b><em>日期</em><br/>
|
||||
<b>[</b><em>日期</em>,<em>日期</em><b>]</b>
|
||||
</p>',
|
||||
|
||||
'Core:AttributeDeadline' => '截止日期',
|
||||
'Core:AttributeDeadline+' => '日期, 显示与当前的相对时间',
|
||||
|
||||
'Core:AttributeExternalKey' => '外键',
|
||||
'Core:AttributeExternalKey+' => 'External (or foreign) key~~',
|
||||
|
||||
'Core:AttributeHierarchicalKey' => 'Hierarchical Key~~',
|
||||
'Core:AttributeHierarchicalKey+' => 'External (or foreign) key to the parent~~',
|
||||
|
||||
'Core:AttributeExternalField' => '外部字段',
|
||||
'Core:AttributeExternalField+' => 'Field mapped to an external key~~',
|
||||
|
||||
'Core:AttributeURL' => 'URL',
|
||||
'Core:AttributeURL+' => 'Absolute or relative URL as a text string~~',
|
||||
|
||||
'Core:AttributeBlob' => 'Blob',
|
||||
'Core:AttributeBlob+' => '任何二进制内容(文档)',
|
||||
|
||||
'Core:AttributeOneWayPassword' => '单向密码',
|
||||
'Core:AttributeOneWayPassword+' => '单向加密(或哈希) 的密码',
|
||||
|
||||
'Core:AttributeTable' => '表',
|
||||
'Core:AttributeTable+' => '带索引的二维数组',
|
||||
|
||||
'Core:AttributePropertySet' => '属性',
|
||||
'Core:AttributePropertySet+' => 'List of untyped properties (name and value)~~',
|
||||
|
||||
'Core:AttributeFriendlyName' => '通用名称',
|
||||
'Core:AttributeFriendlyName+' => 'Attribute created automatically ; the friendly name is computed after several attributes~~',
|
||||
|
||||
'Core:FriendlyName-Label' => '全称',
|
||||
'Core:FriendlyName-Description' => '全称',
|
||||
|
||||
'Core:AttributeTag' => '标签',
|
||||
'Core:AttributeTag+' => '标签',
|
||||
|
||||
'Core:Context=REST/JSON' => 'REST',
|
||||
'Core:Context=Synchro' => '同步',
|
||||
'Core:Context=Setup' => '安装向导',
|
||||
@@ -201,10 +248,10 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:CMDBChangeOp' => '变更操作',
|
||||
'Class:CMDBChangeOp+' => '变更操作跟踪',
|
||||
'Class:CMDBChangeOp' => '变更操作跟踪',
|
||||
'Class:CMDBChangeOp+' => '某人在某时某刻对某个对象的变更操作',
|
||||
'Class:CMDBChangeOp/Attribute:change' => '变更',
|
||||
'Class:CMDBChangeOp/Attribute:change+' => '变更',
|
||||
'Class:CMDBChangeOp/Attribute:change+' => '',
|
||||
'Class:CMDBChangeOp/Attribute:date' => '日期',
|
||||
'Class:CMDBChangeOp/Attribute:date+' => '变更的日期和时间',
|
||||
'Class:CMDBChangeOp/Attribute:userinfo' => '用户',
|
||||
@@ -325,9 +372,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:EventNotification' => '通知事件',
|
||||
'Class:EventNotification+' => 'Trace of a notification that has been sent~~',
|
||||
'Class:EventNotification/Attribute:trigger_id' => '触发器',
|
||||
'Class:EventNotification/Attribute:trigger_id+' => '用户账户',
|
||||
'Class:EventNotification/Attribute:trigger_id+' => '用户账号',
|
||||
'Class:EventNotification/Attribute:action_id' => '用户',
|
||||
'Class:EventNotification/Attribute:action_id+' => '用户账户',
|
||||
'Class:EventNotification/Attribute:action_id+' => '用户账号',
|
||||
'Class:EventNotification/Attribute:object_id' => '对象id',
|
||||
'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)~~',
|
||||
));
|
||||
@@ -408,8 +455,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:EventRestService/Attribute:version+' => '参数 \'版本\'',
|
||||
'Class:EventRestService/Attribute:json_input' => '输入',
|
||||
'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'~~',
|
||||
'Class:EventRestService/Attribute:code' => '代码',
|
||||
'Class:EventRestService/Attribute:code+' => '返回代码',
|
||||
'Class:EventRestService/Attribute:code' => '编码',
|
||||
'Class:EventRestService/Attribute:code+' => '返回编码',
|
||||
'Class:EventRestService/Attribute:json_output' => '响应',
|
||||
'Class:EventRestService/Attribute:json_output+' => 'HTTP 响应 (json)',
|
||||
'Class:EventRestService/Attribute:provider' => '提供者',
|
||||
@@ -660,7 +707,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)~~',
|
||||
'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields~~',
|
||||
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => '目标字段',
|
||||
'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes+' => '',
|
||||
));
|
||||
|
||||
@@ -671,7 +718,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:TriggerOnThresholdReached' => '触发器 (基于阈值)',
|
||||
'Class:TriggerOnThresholdReached+' => '当达到某个阈值时触发',
|
||||
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => '秒表',
|
||||
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => '计时',
|
||||
'Class:TriggerOnThresholdReached/Attribute:stop_watch_code+' => '',
|
||||
'Class:TriggerOnThresholdReached/Attribute:threshold_index' => '阈值',
|
||||
'Class:TriggerOnThresholdReached/Attribute:threshold_index+' => '',
|
||||
@@ -768,6 +815,7 @@ The hyperlink is displayed in the tooltip appearing on the “Lock” symbol of
|
||||
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:administrators' => 'Administrators only',
|
||||
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:everybody' => 'Everybody allowed to delete such objects',
|
||||
'Class:SynchroDataSource/Attribute:user_delete_policy/Value:nobody' => 'Nobody',
|
||||
|
||||
'SynchroDataSource:Description' => '描述',
|
||||
'SynchroDataSource:Reconciliation' => 'Search & reconciliation~~',
|
||||
'SynchroDataSource:Deletion' => 'Deletion rules~~',
|
||||
@@ -1005,8 +1053,9 @@ The hyperlink is displayed in the tooltip appearing on the “Lock” symbol of
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:TagSetFieldData' => '%2$s for class %1$s~~',
|
||||
'Class:TagSetFieldData+' => '~~',
|
||||
'Class:TagSetFieldData/Attribute:code' => '代码',
|
||||
'Class:TagSetFieldData/Attribute:code+' => '内部代码. 必须至少包含3个数字或字母',
|
||||
|
||||
'Class:TagSetFieldData/Attribute:code' => '编码',
|
||||
'Class:TagSetFieldData/Attribute:code+' => '内部编码. 必须至少包含3个数字或字母',
|
||||
'Class:TagSetFieldData/Attribute:label' => '标签',
|
||||
'Class:TagSetFieldData/Attribute:label+' => '显示的标签',
|
||||
'Class:TagSetFieldData/Attribute:description' => '描述',
|
||||
@@ -1014,6 +1063,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class~~~~',
|
||||
'Class:TagSetFieldData/Attribute:obj_class' => 'Object class~~~~',
|
||||
'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code~~~~',
|
||||
|
||||
'Core:TagSetFieldData:ErrorDeleteUsedTag' => '已使用的标签无法删除',
|
||||
'Core:TagSetFieldData:ErrorDuplicateTagCodeOrLabel' => 'Tags codes or labels must be unique~~',
|
||||
'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.~~',
|
||||
@@ -1139,6 +1189,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'Class:ResourceSystemMenu' => 'Resource System Menu~~',
|
||||
'Class:ResourceSystemMenu+' => '',
|
||||
));
|
||||
|
||||
|
||||
// Additional language entries not present in English dict
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', array(
|
||||
'INTERNAL:JQuery-DatePicker:LangCode' => 'zh-CN'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
images/welcome.jpg
Normal file
BIN
images/welcome.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -14,7 +14,6 @@ return array(
|
||||
'AbstractPortalUIExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'AbstractPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'AbstractWeeklyScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
||||
'AbstractWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'Action' => $baseDir . '/core/action.class.inc.php',
|
||||
'ActionChecker' => $baseDir . '/core/userrights.class.inc.php',
|
||||
'ActionEmail' => $baseDir . '/core/action.class.inc.php',
|
||||
@@ -364,8 +363,6 @@ return array(
|
||||
'Combodo\\iTop\\Application\\UI\\Links\\Set\\LinkSetUIBlockFactory' => $baseDir . '/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => $baseDir . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => $baseDir . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
||||
'Combodo\\iTop\\Application\\WelcomePopup\\DefaultWelcomePopup' => $baseDir . '/sources/Application/WelcomePopup/DefaultWelcomePopup.php',
|
||||
'Combodo\\iTop\\Application\\WelcomePopup\\WelcomePopupService' => $baseDir . '/sources/Application/WelcomePopup/WelcomePopupService.php',
|
||||
'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php',
|
||||
'Combodo\\iTop\\Controller\\AbstractController' => $baseDir . '/sources/Controller/AbstractController.php',
|
||||
'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
|
||||
@@ -375,7 +372,6 @@ return array(
|
||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
|
||||
'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => $baseDir . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
|
||||
'Combodo\\iTop\\Controller\\WelcomePopupController' => $baseDir . '/sources/Controller/WelcomePopupController.php',
|
||||
'Combodo\\iTop\\Controller\\iController' => $baseDir . '/sources/Controller/iController.php',
|
||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => $baseDir . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
||||
@@ -387,6 +383,7 @@ return array(
|
||||
'Combodo\\iTop\\Core\\Email\\EmailFactory' => $baseDir . '/sources/Core/Email/EmailFactory.php',
|
||||
'Combodo\\iTop\\Core\\Email\\iEMail' => $baseDir . '/sources/Core/Email/iEMail.php',
|
||||
'Combodo\\iTop\\Core\\EventListener\\AttributeBlobEventListener' => $baseDir . '/sources/Core/EventListener/AttributeBlobEventListener.php',
|
||||
'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => $baseDir . '/sources/Core/Kpi/KpiLogData.php',
|
||||
'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => $baseDir . '/sources/Core/MetaModel/FriendlyNameType.php',
|
||||
'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => $baseDir . '/sources/Core/MetaModel/HierarchicalKey.php',
|
||||
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
|
||||
@@ -465,6 +462,7 @@ return array(
|
||||
'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php',
|
||||
'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php',
|
||||
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
|
||||
@@ -2956,6 +2954,7 @@ return array(
|
||||
'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
|
||||
'iDisplay' => $baseDir . '/core/dbobject.class.php',
|
||||
'iFieldRendererMappingsExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iKPILoggerExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iKeyboardShortcut' => $baseDir . '/sources/Application/UI/Hook/iKeyboardShortcut.php',
|
||||
'iLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
|
||||
'iLoginExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
@@ -2986,7 +2985,6 @@ return array(
|
||||
'iTopWebPage' => $baseDir . '/sources/Application/WebPage/iTopWebPage.php',
|
||||
'iTopWizardWebPage' => $baseDir . '/sources/Application/WebPage/iTopWizardWebPage.php',
|
||||
'iTopXmlException' => $baseDir . '/application/exceptions/iTopXmlException.php',
|
||||
'iWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iWorkingTimeComputer' => $baseDir . '/core/computing.inc.php',
|
||||
'lnkAuditCategoryToAuditDomain' => $baseDir . '/application/audit.domain.class.inc.php',
|
||||
'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php',
|
||||
|
||||
@@ -378,7 +378,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'AbstractPortalUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'AbstractPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'AbstractWeeklyScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
||||
'AbstractWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'Action' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||
'ActionChecker' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
|
||||
'ActionEmail' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||
@@ -728,8 +727,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Application\\UI\\Links\\Set\\LinkSetUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => __DIR__ . '/../..' . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => __DIR__ . '/../..' . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
||||
'Combodo\\iTop\\Application\\WelcomePopup\\DefaultWelcomePopup' => __DIR__ . '/../..' . '/sources/Application/WelcomePopup/DefaultWelcomePopup.php',
|
||||
'Combodo\\iTop\\Application\\WelcomePopup\\WelcomePopupService' => __DIR__ . '/../..' . '/sources/Application/WelcomePopup/WelcomePopupService.php',
|
||||
'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php',
|
||||
'Combodo\\iTop\\Controller\\AbstractController' => __DIR__ . '/../..' . '/sources/Controller/AbstractController.php',
|
||||
'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
|
||||
@@ -739,7 +736,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
|
||||
'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => __DIR__ . '/../..' . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
|
||||
'Combodo\\iTop\\Controller\\WelcomePopupController' => __DIR__ . '/../..' . '/sources/Controller/WelcomePopupController.php',
|
||||
'Combodo\\iTop\\Controller\\iController' => __DIR__ . '/../..' . '/sources/Controller/iController.php',
|
||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
||||
@@ -751,6 +747,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Core\\Email\\EmailFactory' => __DIR__ . '/../..' . '/sources/Core/Email/EmailFactory.php',
|
||||
'Combodo\\iTop\\Core\\Email\\iEMail' => __DIR__ . '/../..' . '/sources/Core/Email/iEMail.php',
|
||||
'Combodo\\iTop\\Core\\EventListener\\AttributeBlobEventListener' => __DIR__ . '/../..' . '/sources/Core/EventListener/AttributeBlobEventListener.php',
|
||||
'Combodo\\iTop\\Core\\Kpi\\KpiLogData' => __DIR__ . '/../..' . '/sources/Core/Kpi/KpiLogData.php',
|
||||
'Combodo\\iTop\\Core\\MetaModel\\FriendlyNameType' => __DIR__ . '/../..' . '/sources/Core/MetaModel/FriendlyNameType.php',
|
||||
'Combodo\\iTop\\Core\\MetaModel\\HierarchicalKey' => __DIR__ . '/../..' . '/sources/Core/MetaModel/HierarchicalKey.php',
|
||||
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
||||
@@ -829,6 +826,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php',
|
||||
'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php',
|
||||
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
|
||||
@@ -3320,6 +3318,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
|
||||
'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php',
|
||||
'iFieldRendererMappingsExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iKPILoggerExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iKeyboardShortcut' => __DIR__ . '/../..' . '/sources/Application/UI/Hook/iKeyboardShortcut.php',
|
||||
'iLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
||||
'iLoginExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
@@ -3350,7 +3349,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'iTopWebPage' => __DIR__ . '/../..' . '/sources/Application/WebPage/iTopWebPage.php',
|
||||
'iTopWizardWebPage' => __DIR__ . '/../..' . '/sources/Application/WebPage/iTopWizardWebPage.php',
|
||||
'iTopXmlException' => __DIR__ . '/../..' . '/application/exceptions/iTopXmlException.php',
|
||||
'iWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iWorkingTimeComputer' => __DIR__ . '/../..' . '/core/computing.inc.php',
|
||||
'lnkAuditCategoryToAuditDomain' => __DIR__ . '/../..' . '/application/audit.domain.class.inc.php',
|
||||
'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '204a6d8e51619cd669c6d9d7722edaa36d1c3394',
|
||||
'reference' => 'dbf3393c9729a20f0bf389d343507238d61fef56',
|
||||
'name' => 'combodo/itop',
|
||||
'dev' => true,
|
||||
),
|
||||
@@ -25,7 +25,7 @@
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '204a6d8e51619cd669c6d9d7722edaa36d1c3394',
|
||||
'reference' => 'dbf3393c9729a20f0bf389d343507238d61fef56',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'combodo/tcpdf' => array(
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<security>
|
||||
<requestFiltering>
|
||||
<fileExtensions applyToWebDAV="false" allowUnlisted="false"></fileExtensions>
|
||||
</requestFiltering>
|
||||
<authorization>
|
||||
<deny users="*" /> <!-- Denies all users -->
|
||||
</authorization>
|
||||
</security>
|
||||
</system.webServer>
|
||||
<system.web>
|
||||
<authorization>
|
||||
<deny users="*" /> <!-- Denies all users -->
|
||||
</authorization>
|
||||
</system.web>
|
||||
</configuration>
|
||||
28
pages/UI.php
28
pages/UI.php
@@ -20,7 +20,6 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Controller\Base\Layout\ObjectController;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
|
||||
/**
|
||||
* Displays a popup welcome message, once per session at maximum
|
||||
@@ -30,18 +29,19 @@ use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function DisplayWelcomePopup(WebPage $oP): void
|
||||
function DisplayWelcomePopup(WebPage $oP)
|
||||
{
|
||||
if (!Session::IsSet('welcome'))
|
||||
{
|
||||
$oWelcomePopupService = new WelcomePopupService();
|
||||
$aMessages = $oWelcomePopupService->GetMessages();
|
||||
if (count($aMessages) > 0)
|
||||
// Check, only once per session, if the popup should be displayed...
|
||||
// If the user did not already ask for hiding it forever
|
||||
$bPopup = appUserPreferences::GetPref('welcome_popup', true);
|
||||
if ($bPopup)
|
||||
{
|
||||
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup', ['messages' => $aMessages]);
|
||||
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup');
|
||||
Session::Set('welcome', 'ok');
|
||||
}
|
||||
Session::Set('welcome', 'ok'); // Try just once per session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction)
|
||||
}
|
||||
// Get the list of missing mandatory fields for the target state, considering only the changes from the previous form (i.e don't prompt twice)
|
||||
$aExpectedAttributes = $oObj->GetTransitionAttributes($sNextAction);
|
||||
|
||||
|
||||
if (count($aExpectedAttributes) == 0)
|
||||
{
|
||||
// If all the mandatory fields are already present, just apply the transition silently...
|
||||
@@ -85,7 +85,7 @@ function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction)
|
||||
// redirect to the 'stimulus' action
|
||||
$oAppContext = new ApplicationContext();
|
||||
//echo "<p>Missing Attributes <pre>".print_r($aExpectedAttributes, true)."</pre></p>\n";
|
||||
|
||||
|
||||
$oP->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=stimulus&class='.get_class($oObj).'&stimulus='.$sNextAction.'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink());
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ function DisplayMultipleSelectionForm(WebPage $oP, DBSearch $oFilter, string $sN
|
||||
$aExtraParams['surround_with_panel'] = true;
|
||||
if(array_key_exists('icon', $aDisplayParams)){
|
||||
$aExtraParams['panel_icon'] = $aDisplayParams['icon'];
|
||||
}
|
||||
}
|
||||
if(array_key_exists('title', $aDisplayParams)){
|
||||
$aExtraParams['panel_title'] = $aDisplayParams['title'];
|
||||
}
|
||||
@@ -292,7 +292,7 @@ function DisplayNavigatorGroupTab($oP)
|
||||
}
|
||||
|
||||
/***********************************************************************************
|
||||
*
|
||||
*
|
||||
* Main user interface page starts here
|
||||
*
|
||||
***********************************************************************************/
|
||||
@@ -700,7 +700,7 @@ try
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/** @deprecated 3.1.0 Use the "object.new" route instead */
|
||||
// Kept for backward compatibility
|
||||
case 'new': // Form to create a new object
|
||||
@@ -1638,4 +1638,4 @@ class UI
|
||||
);
|
||||
cmdbAbstractObject::DoBulkModify($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $bPreview, $sCancelUrl, $aContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
|
||||
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager;
|
||||
use Combodo\iTop\Controller\WelcomePopupController;
|
||||
|
||||
require_once('../approot.inc.php');
|
||||
|
||||
@@ -202,10 +201,10 @@ try
|
||||
$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
|
||||
$oAppContext = new ApplicationContext();
|
||||
$aPrefillFormParam = array(
|
||||
'user' => Session::Get("auth_user"),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'user' => Session::Get("auth_user"),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'source_obj' => $oObj
|
||||
);
|
||||
$aAlreadyLinked = utils::ReadParam('aAlreadyLinked', array());
|
||||
@@ -277,10 +276,10 @@ try
|
||||
$oPage->SetContentType('text/html');
|
||||
$oAppContext = new ApplicationContext();
|
||||
$aPrefillFormParam = array(
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'source_obj' => $oObj,
|
||||
);
|
||||
$aPrefillFormParam['dest_class'] = ($oObj === null ? '' : $oObj->Get($sAttCode)->GetClass());
|
||||
@@ -304,10 +303,10 @@ try
|
||||
}
|
||||
$oAppContext = new ApplicationContext();
|
||||
$aPrefillFormParam = array(
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'origin' => 'console',
|
||||
'source_obj' => $oObj,
|
||||
);
|
||||
$aPrefillFormParam['dest_class'] = ($oObj === null ? '' : $oObj->Get($sAttCode)->GetClass());
|
||||
@@ -419,10 +418,10 @@ try
|
||||
$iInputId = utils::ReadParam('iInputId', '');
|
||||
$sAttCode = utils::ReadParam('sAttCode', '');
|
||||
$sJson = utils::ReadParam('json', '', false, 'raw_data');
|
||||
$bTargetClassSelected = utils::ReadParam('bTargetClassSelected', '', false, 'raw_data');
|
||||
// Building form, if target class has child classes we ask the user for the desired leaf class, unless we've already done just that
|
||||
$bTargetClassSelected = utils::ReadParam('bTargetClassSelected', '', false, 'raw_data');
|
||||
// Building form, if target class has child classes we ask the user for the desired leaf class, unless we've already done just that
|
||||
$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
|
||||
if (!$bTargetClassSelected && MetaModel::HasChildrenClasses($sTargetClass)) {
|
||||
if(!$bTargetClassSelected && MetaModel::HasChildrenClasses($sTargetClass)){
|
||||
$oWidget->GetClassSelectionForm($oPage);
|
||||
} else {
|
||||
$aPrefillFormParam = array();
|
||||
@@ -431,11 +430,11 @@ try
|
||||
$oObj = $oWizardHelper->GetTargetObject();
|
||||
$oAppContext = new ApplicationContext();
|
||||
$aPrefillFormParam = array(
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'user' => Session::Get('auth_user'),
|
||||
'context' => $oAppContext->GetAsHash(),
|
||||
'att_code' => $sAttCode,
|
||||
'source_obj' => $oObj,
|
||||
'origin' => 'console'
|
||||
'origin' => 'console'
|
||||
);
|
||||
} else {
|
||||
// Search form: no current object
|
||||
@@ -528,8 +527,7 @@ try
|
||||
$oObj->Set($sAttCode, $defaultValue);
|
||||
}
|
||||
$sFormPrefix = $oWizardHelper->GetFormPrefix();
|
||||
$aExpectedAttributes = ($oWizardHelper->GetStimulus() === null) ? array() : $oObj->GetTransitionAttributes($oWizardHelper->GetStimulus(),
|
||||
$oWizardHelper->GetInitialState());
|
||||
$aExpectedAttributes = ($oWizardHelper->GetStimulus() === null) ? array() : $oObj->GetTransitionAttributes($oWizardHelper->GetStimulus(), $oWizardHelper->GetInitialState());
|
||||
foreach ($oWizardHelper->GetFieldsForAllowedValues() as $sAttCode) {
|
||||
$sId = $oWizardHelper->GetIdForField($sAttCode);
|
||||
if ($sId != '') {
|
||||
@@ -575,8 +573,7 @@ try
|
||||
$sTargetState = utils::ReadParam('target_state', '');
|
||||
$iTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id');
|
||||
$oObj->Set(MetaModel::GetStateAttributeCode($sClass), $sTargetState);
|
||||
cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, array(),
|
||||
array('action' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php', 'transaction_id' => $iTransactionId));
|
||||
cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, array(), array('action' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php', 'transaction_id' => $iTransactionId));
|
||||
break;
|
||||
|
||||
// DisplayBlock
|
||||
@@ -603,7 +600,8 @@ try
|
||||
} else {
|
||||
try {
|
||||
$oFilter = DBSearch::unserialize($sFilter);
|
||||
} catch (CoreException $e) {
|
||||
}
|
||||
catch (CoreException $e) {
|
||||
$sFilter = utils::HtmlEntities($sFilter);
|
||||
$oPage->p("Invalid query (invalid filter) : <code>$sFilter</code>");
|
||||
IssueLog::Error("ajax.render operation='ajax', invalid DBSearch filter param : $sFilter");
|
||||
@@ -668,8 +666,7 @@ try
|
||||
$aResult['JSURLs'] = str_replace('"', '\'', $oBlock->sJSURLs);
|
||||
$aResult['js'] = 'charts['.$iRefresh.'].load({json: '.str_replace('"', '\'', $oBlock->sJson).
|
||||
',keys: { x: \'label\', value: [\'value\']'.
|
||||
'},onclick: function (d) { var aURLs = $.parseJSON('.str_replace('"', '\'',
|
||||
$oBlock->sJSURLs).'); window.location.href= aURLs[d.index]; }})';
|
||||
'},onclick: function (d) { var aURLs = $.parseJSON('.str_replace('"', '\'', $oBlock->sJSURLs).'); window.location.href= aURLs[d.index]; }})';
|
||||
break;
|
||||
|
||||
case 'pie':
|
||||
@@ -680,8 +677,7 @@ try
|
||||
$aResult['JSURLs'] = str_replace('"', '\'', $oBlock->sJSURLs);
|
||||
$aResult['js'] = 'charts['.$iRefresh.'].load({columns: '.str_replace('"', '\'', $oBlock->sJSColumns).
|
||||
',names: '.str_replace('"', '\'', $oBlock->sJSNames).
|
||||
',onclick: function (d) { var aURLs = $.parseJSON('.str_replace('"', '\'',
|
||||
$oBlock->sJSURLs).'); window.location.href= aURLs[d.index]; }})';
|
||||
',onclick: function (d) { var aURLs = $.parseJSON('.str_replace('"', '\'', $oBlock->sJSURLs).'); window.location.href= aURLs[d.index]; }})';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -781,14 +777,14 @@ try
|
||||
$oFilter = new DBObjectSearch($sClass);
|
||||
$oSet = new CMDBObjectSet($oFilter);
|
||||
$sHtml = cmdbAbstractObject::GetSearchForm($oPage, $oSet, array(
|
||||
'currentId' => $currentId,
|
||||
'baseClass' => $sRootClass,
|
||||
'action' => $sAction,
|
||||
'table_id' => $sTableId,
|
||||
'selection_mode' => $sSelectionMode,
|
||||
'currentId' => $currentId,
|
||||
'baseClass' => $sRootClass,
|
||||
'action' => $sAction,
|
||||
'table_id' => $sTableId,
|
||||
'selection_mode' => $sSelectionMode,
|
||||
'result_list_outer_selector' => $sResultListOuterSelector,
|
||||
'cssCount' => $scssCount,
|
||||
'table_inner_id' => $sTableInnerId
|
||||
'cssCount' => $scssCount,
|
||||
'table_inner_id' => $sTableInnerId
|
||||
));
|
||||
$oPage->add($sHtml);
|
||||
break;
|
||||
@@ -825,13 +821,13 @@ try
|
||||
TemporaryObjectManager::GetInstance()->CancelAllTemporaryObjects($iTransactionId);
|
||||
|
||||
IssueLog::Trace('on_form_cancel', $sObjClass, array(
|
||||
'$iObjKey' => $iObjKey,
|
||||
'$iObjKey' => $iObjKey,
|
||||
'$sTransactionId' => $iTransactionId,
|
||||
'$sTempId' => $sTempId,
|
||||
'$sToken' => $sToken,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
'$sTempId' => $sTempId,
|
||||
'$sToken' => $sToken,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
|
||||
break;
|
||||
@@ -883,9 +879,11 @@ try
|
||||
$oDoc = utils::ReadPostedDocument('dashboard_upload_file');
|
||||
$oDashboard->FromXml($oDoc->GetData());
|
||||
$oDashboard->Save();
|
||||
} catch (DOMException $e) {
|
||||
}
|
||||
catch (DOMException $e) {
|
||||
$aResult = array('error' => Dict::S('UI:Error:InvalidDashboardFile'));
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$aResult = array('error' => $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
@@ -1056,8 +1054,7 @@ EOF
|
||||
$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
|
||||
// but is executed BEFORE all 'ready_scripts'
|
||||
$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php',
|
||||
array('operation' => 'update_dashlet_property'));
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
|
||||
$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
|
||||
$sHtml = str_replace("\n", '', $sHtml);
|
||||
$sHtml = str_replace("\r", '', $sHtml);
|
||||
@@ -1116,8 +1113,7 @@ EOF
|
||||
}
|
||||
if ($oDashlet->IsFormRedrawNeeded()) {
|
||||
$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php',
|
||||
array('operation' => 'update_dashlet_property', 'extra_params' => $aExtraParams));
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property', 'extra_params' => $aExtraParams));
|
||||
$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true, '.itop-dashboard'));
|
||||
$sHtml = str_replace("\n", '', $sHtml);
|
||||
$sHtml = str_replace("\r", '', $sHtml);
|
||||
@@ -1363,8 +1359,7 @@ JS
|
||||
}
|
||||
|
||||
$sFullTextJS = addslashes($sFullText);
|
||||
$bEnableEnlarge = array_key_exists($sClassName, $aAccelerators) && array_key_exists('query',
|
||||
$aAccelerators[$sClassName]);
|
||||
$bEnableEnlarge = array_key_exists($sClassName, $aAccelerators) && array_key_exists('query', $aAccelerators[$sClassName]);
|
||||
if (array_key_exists($sClassName, $aAccelerators) && array_key_exists('enable_enlarge', $aAccelerators[$sClassName])) {
|
||||
$bEnableEnlarge &= $aAccelerators[$sClassName]['enable_enlarge'];
|
||||
}
|
||||
@@ -1398,11 +1393,9 @@ EOF;
|
||||
$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
|
||||
$oPage->add("<div class=\"page_header\">\n");
|
||||
if (array_key_exists($sClassName, $aAccelerators)) {
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found',
|
||||
count($aLeafs), Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
|
||||
} else {
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found',
|
||||
count($aLeafs), Metamodel::GetName($sClassName))."</h2>\n");
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aLeafs), Metamodel::GetName($sClassName))."</h2>\n");
|
||||
}
|
||||
$oPage->add("</div>\n");
|
||||
$oLeafsFilter->AddCondition('id', $aLeafs, 'IN');
|
||||
@@ -1418,8 +1411,7 @@ EOF;
|
||||
if (array_key_exists($sClassName, $aAccelerators)) {
|
||||
$oPage->add("<div class=\"search-class-result search-class-$sClassName\">\n");
|
||||
$oPage->add("<div class=\"page_header\">\n");
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found',
|
||||
0, Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
|
||||
$oPage->add('<h2 class="ibo-global-search--result--title">'.MetaModel::GetClassIcon($sClassName).Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', 0, Metamodel::GetName($sClassName)).$sEnlargeButton."</h2>\n");
|
||||
$oPage->add("</div>\n");
|
||||
$oPage->add("</div>\n");
|
||||
$oPage->p(' '); // Some space ?
|
||||
@@ -1499,8 +1491,7 @@ EOF
|
||||
$oFilter->SetShowObsoleteData(utils::ShowObsoleteData());
|
||||
$oSet = new DBObjectSet($oFilter);
|
||||
$oPage->add("<div class=\"page_header\">\n");
|
||||
$oPage->add("<h2>".MetaModel::GetClassIcon($sClass)." <span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found',
|
||||
$oSet->Count(), Metamodel::GetName($sClass))."</h2>\n");
|
||||
$oPage->add("<h2>".MetaModel::GetClassIcon($sClass)." <span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sClass))."</h2>\n");
|
||||
$oPage->add("</div>\n");
|
||||
if ($oSet->Count() > 0) {
|
||||
$aLeafs = array();
|
||||
@@ -1588,11 +1579,10 @@ EOF
|
||||
$oPage->add('<div class="statistics"><div class="stats-toggle closed">'.Dict::S('ExcelExport:Statistics').'<div class="stats-data"></div></div></div>');
|
||||
$oPage->add('</div>');
|
||||
$aLabels = array(
|
||||
'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
|
||||
'cancel_button' => Dict::S('UI:Button:Cancel'),
|
||||
'export_button' => Dict::S('ExcelExporter:ExportButton'),
|
||||
'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'),
|
||||
//TODO: better name for the file (based on the class of the filter??)
|
||||
'dialog_title' => Dict::S('ExcelExporter:ExportDialogTitle'),
|
||||
'cancel_button' => Dict::S('UI:Button:Cancel'),
|
||||
'export_button' => Dict::S('ExcelExporter:ExportButton'),
|
||||
'download_button' => Dict::Format('ExcelExporter:DownloadButton', 'export.xlsx'), //TODO: better name for the file (based on the class of the filter??)
|
||||
);
|
||||
$sJSLabels = json_encode($aLabels);
|
||||
$sFilter = addslashes($sFilter);
|
||||
@@ -1704,8 +1694,7 @@ EOF
|
||||
if ($sDirection == 'up') {
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
|
||||
} else {
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects,
|
||||
$aContexts);
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
|
||||
}
|
||||
|
||||
// Remove excluded classes from the graph
|
||||
@@ -1765,11 +1754,9 @@ EOF
|
||||
$sIconUrl = MetaModel::GetClassIcon($sListClass, false);
|
||||
$sIconUrl = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
|
||||
$oTitle = new Html("<img src=\"$sIconUrl\" style=\"vertical-align:middle;width: 24px; height: 24px;\"/> ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sListClass)));*/
|
||||
$oTitle = new Html(Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(),
|
||||
Metamodel::GetName($sListClass)));
|
||||
$oTitle = new Html(Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', $oSet->Count(), Metamodel::GetName($sListClass)));
|
||||
$oPage->AddSubBlock(TitleUIBlockFactory::MakeStandard($oTitle, 2));
|
||||
$oPage->AddSubBlock(cmdbAbstractObject::GetDataTableFromDBObjectSet($oSet,
|
||||
array('table_id' => $sSourceClass.'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)));
|
||||
$oPage->AddSubBlock(cmdbAbstractObject::GetDataTableFromDBObjectSet($oSet, array('table_id' => $sSourceClass.'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)));
|
||||
}
|
||||
|
||||
// Then the content of the groups (one table per group)
|
||||
@@ -1781,10 +1768,8 @@ EOF
|
||||
$sListClass = get_class(current($aObjects));
|
||||
$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
|
||||
$sIconUrl = MetaModel::GetClassIcon($sListClass, false);
|
||||
$sIconUrl = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/',
|
||||
$sIconUrl);
|
||||
$oTitle = new Html("<img src=\"$sIconUrl\" style=\"vertical-align:middle;width: 24px; height: 24px;\"/> ".Dict::Format('UI:RelationGroupNumber_N',
|
||||
(1 + $idx)), Metamodel::GetName($sListClass));
|
||||
$sIconUrl = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
|
||||
$oTitle = new Html("<img src=\"$sIconUrl\" style=\"vertical-align:middle;width: 24px; height: 24px;\"/> ".Dict::Format('UI:RelationGroupNumber_N', (1 + $idx)), Metamodel::GetName($sListClass));
|
||||
$oPage->AddSubBlock(TitleUIBlockFactory::MakeStandard($oTitle, 2));
|
||||
$oPage->AddSubBlock(cmdbAbstractObject::GetDataTableFromDBObjectSet($oSet));
|
||||
|
||||
@@ -1858,8 +1843,7 @@ EOF
|
||||
if ($sDirection == 'up') {
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aContexts);
|
||||
} else {
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects,
|
||||
$aContexts);
|
||||
$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth, true, $aExcludedObjects, $aContexts);
|
||||
}
|
||||
|
||||
// Remove excluded classes from the graph
|
||||
@@ -1890,15 +1874,13 @@ EOF
|
||||
$oSearch = new DBObjectSearch($sListClass);
|
||||
$oSearch->AddCondition('id', $aDefinition['keys'], 'IN');
|
||||
$oSearch->SetShowObsoleteData(utils::ShowObsoleteData());
|
||||
$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral(Dict::Format('UI:RelationGroupNumber_N', (1 + $idx)), 1,
|
||||
"relation_group_$idx"));
|
||||
$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral(Dict::Format('UI:RelationGroupNumber_N', (1 + $idx)), 1, "relation_group_$idx"));
|
||||
$oBlock = new DisplayBlock($oSearch, 'list');
|
||||
$oBlock->Display($oPage, 'group_'.$iBlock++, array(
|
||||
'surround_with_panel' => true,
|
||||
'panel_class' => $sListClass,
|
||||
'panel_title' => Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aDefinition['keys']),
|
||||
Metamodel::GetName($sListClass)),
|
||||
'panel_icon' => MetaModel::GetClassIcon($sListClass, false),
|
||||
'panel_class' => $sListClass,
|
||||
'panel_title' => Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aDefinition['keys']), Metamodel::GetName($sListClass)),
|
||||
'panel_icon' => MetaModel::GetClassIcon($sListClass, false),
|
||||
));
|
||||
}
|
||||
break;
|
||||
@@ -1912,12 +1894,11 @@ EOF
|
||||
$oSearch->SetShowObsoleteData(utils::ShowObsoleteData());
|
||||
$oBlock = new DisplayBlock($oSearch, 'list');
|
||||
$oBlock->Display($oPage, 'list_'.$iBlock++, array(
|
||||
'table_id' => 'ImpactAnalysis_'.$sListClass,
|
||||
'table_id' => 'ImpactAnalysis_'.$sListClass,
|
||||
'surround_with_panel' => true,
|
||||
'panel_class' => $sListClass,
|
||||
'panel_title' => Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aKeys),
|
||||
Metamodel::GetName($sListClass)),
|
||||
'panel_icon' => MetaModel::GetClassIcon($sListClass, false),
|
||||
'panel_class' => $sListClass,
|
||||
'panel_title' => Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aKeys), Metamodel::GetName($sListClass)),
|
||||
'panel_icon' => MetaModel::GetClassIcon($sListClass, false),
|
||||
));
|
||||
}
|
||||
break;
|
||||
@@ -1969,8 +1950,7 @@ EOF
|
||||
|
||||
$sContextKey = 'itop-tickets/relation_context/'.$sClass.'/'.$sRelation.'/'.$sDirection;
|
||||
$oAppContext = new ApplicationContext();
|
||||
$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey,
|
||||
array('this' => $oTicket));
|
||||
$oGraph->Display($oPage, $aResults, $sRelation, $oAppContext, $aExcludedObjects, $sClass, $iId, $sContextKey, array('this' => $oTicket));
|
||||
break;
|
||||
|
||||
case 'export_build':
|
||||
@@ -2020,7 +2000,7 @@ EOF
|
||||
$aLockData = iTopOwnershipLock::IsLocked($sObjClass, $iObjKey);
|
||||
|
||||
$aResult = [
|
||||
'locked' => $aLockData['locked'],
|
||||
'locked' => $aLockData['locked'],
|
||||
'message' => '',
|
||||
];
|
||||
|
||||
@@ -2107,11 +2087,11 @@ EOF
|
||||
$aResult = array(
|
||||
'uploaded' => 0,
|
||||
'fileName' => '',
|
||||
'url' => '',
|
||||
'icon' => '',
|
||||
'msg' => '',
|
||||
'att_id' => 0,
|
||||
'preview' => 'false',
|
||||
'url' => '',
|
||||
'icon' => '',
|
||||
'msg' => '',
|
||||
'att_id' => 0,
|
||||
'preview' => 'false',
|
||||
);
|
||||
|
||||
$sObjClass = stripslashes(utils::ReadParam('obj_class', '', false, 'class'));
|
||||
@@ -2146,19 +2126,20 @@ EOF
|
||||
}
|
||||
|
||||
IssueLog::Trace('InlineImage created', LogChannels::INLINE_IMAGE, array(
|
||||
'$operation' => $operation,
|
||||
'$aResult' => $aResult,
|
||||
'secret' => $oAttachment->Get('secret'),
|
||||
'temp_id' => $sTempId,
|
||||
'item_class' => $sObjClass,
|
||||
'user' => UserRights::GetUser(),
|
||||
'$operation' => $operation,
|
||||
'$aResult' => $aResult,
|
||||
'secret' => $oAttachment->Get('secret'),
|
||||
'temp_id' => $sTempId,
|
||||
'item_class' => $sObjClass,
|
||||
'user' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
} else {
|
||||
$aResult['error'] = $oDoc->GetFileName().' is not a valid image format.';
|
||||
}
|
||||
} catch (FileUploadException $e) {
|
||||
}
|
||||
catch (FileUploadException $e) {
|
||||
$aResult['error'] = $e->GetMessage();
|
||||
}
|
||||
}
|
||||
@@ -2175,8 +2156,8 @@ EOF
|
||||
if (!InlineImage::IsImage($sDocMimeType)) {
|
||||
LogErrorMessage('CKE : error when uploading image in ajax.render.php, not an image',
|
||||
array(
|
||||
'operation' => 'cke_upload_and_browse',
|
||||
'class' => $sObjClass,
|
||||
'operation' => 'cke_upload_and_browse',
|
||||
'class' => $sObjClass,
|
||||
'ImgMimeType' => $sDocMimeType,
|
||||
));
|
||||
} else {
|
||||
@@ -2193,21 +2174,22 @@ EOF
|
||||
$iAttId = $oAttachment->DBInsert();
|
||||
|
||||
IssueLog::Trace('InlineImage created', LogChannels::INLINE_IMAGE, array(
|
||||
'$operation' => $operation,
|
||||
'secret' => $oAttachment->Get('secret'),
|
||||
'temp_id' => $sTempId,
|
||||
'item_class' => $sObjClass,
|
||||
'user' => UserRights::GetUser(),
|
||||
'$operation' => $operation,
|
||||
'secret' => $oAttachment->Get('secret'),
|
||||
'temp_id' => $sTempId,
|
||||
'item_class' => $sObjClass,
|
||||
'user' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
}
|
||||
|
||||
} catch (FileUploadException $e) {
|
||||
}
|
||||
catch (FileUploadException $e) {
|
||||
LogErrorMessage('CKE : error when uploading image in ajax.render.php, exception occured',
|
||||
array(
|
||||
'operation' => 'cke_upload_and_browse',
|
||||
'class' => $sObjClass,
|
||||
'operation' => 'cke_upload_and_browse',
|
||||
'class' => $sObjClass,
|
||||
'exceptionMsg' => $e,
|
||||
));
|
||||
}
|
||||
@@ -2321,8 +2303,7 @@ $('.img-picker').magnificPopup({type: 'image', closeOnContentClick: true });
|
||||
EOF
|
||||
);
|
||||
$sOQL = "SELECT InlineImage WHERE ((temp_id = :temp_id) OR (item_class = :obj_class AND item_id = :obj_id))";
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(),
|
||||
array('temp_id' => $sTempId, 'obj_class' => $sClass, 'obj_id' => $iObjectId));
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array('temp_id' => $sTempId, 'obj_class' => $sClass, 'obj_id' => $iObjectId));
|
||||
$oPage->add("<div><fieldset><legend>$sAvailableImagesLegend</legend>");
|
||||
|
||||
if ($oSet->Count() == 0) {
|
||||
@@ -2379,8 +2360,7 @@ EOF
|
||||
$aTriggerMentionedSearches = [];
|
||||
|
||||
$aTriggerSetParams = array('class_list' => MetaModel::EnumParentClasses($sHostClass, ENUM_PARENT_CLASSES_ALL));
|
||||
$oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"),
|
||||
array(), $aTriggerSetParams);
|
||||
$oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), array(), $aTriggerSetParams);
|
||||
/** @var \TriggerOnObjectMention $oTrigger */
|
||||
while ($oTrigger = $oTriggerSet->Fetch()) {
|
||||
$sTriggerMentionedOQL = $oTrigger->Get('mentioned_filter');
|
||||
@@ -2414,8 +2394,7 @@ EOF
|
||||
|
||||
// Add condition to filter on the friendlyname
|
||||
$oSearch->AddConditionExpression(
|
||||
new BinaryExpression(new FieldExpression('friendlyname', $sSearchMainClassAlias), 'LIKE',
|
||||
new VariableExpression('needle'))
|
||||
new BinaryExpression(new FieldExpression('friendlyname', $sSearchMainClassAlias), 'LIKE', new VariableExpression('needle'))
|
||||
);
|
||||
|
||||
$oSet = new DBObjectSet($oSearch, [], $aSearchParams);
|
||||
@@ -2434,8 +2413,8 @@ EOF
|
||||
$sObjectClass = get_class($oObject);
|
||||
$iObjectId = $oObject->GetKey();
|
||||
$aMatch = [
|
||||
'class' => $sObjectClass,
|
||||
'id' => $iObjectId,
|
||||
'class' => $sObjectClass,
|
||||
'id' => $iObjectId,
|
||||
'friendlyname' => $oObject->Get('friendlyname'),
|
||||
];
|
||||
|
||||
@@ -2444,8 +2423,7 @@ EOF
|
||||
/** @var \ormDocument $oImage */
|
||||
$oImage = $oObject->Get($sObjectImageAttCode);
|
||||
if (!$oImage->IsEmpty()) {
|
||||
$aMatch['picture_style'] = "background-image: url('".$oImage->GetDisplayURL($sObjectClass, $iObjectId,
|
||||
$sObjectImageAttCode)."')";
|
||||
$aMatch['picture_style'] = "background-image: url('".$oImage->GetDisplayURL($sObjectClass, $iObjectId, $sObjectImageAttCode)."')";
|
||||
$aMatch['initials'] = '';
|
||||
} else {
|
||||
// If no image found, fallback on initials
|
||||
@@ -2482,7 +2460,8 @@ EOF
|
||||
$aRenderRes = $oRenderer->Render($aRequestedFields);
|
||||
|
||||
$aResult['form']['updated_fields'] = $aRenderRes;
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$aResult['error'] = $e->getMessage();
|
||||
}
|
||||
$oPage->SetData($aResult);
|
||||
@@ -2499,9 +2478,10 @@ EOF
|
||||
$oController = new PreferencesController();
|
||||
$aResult = $oController->SetUserPicture();
|
||||
$aResult['success'] = true;
|
||||
} catch (Exception $oException) {
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
@@ -2520,9 +2500,10 @@ EOF
|
||||
$aResult = [
|
||||
'success' => true,
|
||||
];
|
||||
} catch (Exception $oException) {
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
@@ -2535,9 +2516,10 @@ EOF
|
||||
try {
|
||||
$oController = new ActivityPanelController();
|
||||
$aResult = $oController->AddCaseLogsEntries();
|
||||
} catch (Exception $oException) {
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
@@ -2550,9 +2532,10 @@ EOF
|
||||
try {
|
||||
$oController = new ActivityPanelController();
|
||||
$aResult = $oController->LoadMoreEntries();
|
||||
} catch (Exception $oException) {
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
@@ -2568,24 +2551,6 @@ EOF
|
||||
$oAjaxRenderController->GetMenusCount($oPage);
|
||||
break;
|
||||
|
||||
//--------------------------------
|
||||
// WelcomePopupMenu
|
||||
//--------------------------------
|
||||
case 'welcome_popup_acknowledge_message':
|
||||
$oPage = new JsonPage();
|
||||
try {
|
||||
$oController = new WelcomePopupController();
|
||||
$oController->AcknowledgeMessage();
|
||||
$aResult = ['success' => true];
|
||||
} catch (Exception $oException) {
|
||||
$aResult = [
|
||||
'success' => false,
|
||||
'error_message' => $oException->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aResult);
|
||||
break;
|
||||
|
||||
//--------------------------------
|
||||
// Object
|
||||
//--------------------------------
|
||||
|
||||
@@ -141,58 +141,55 @@ JS
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
$bIsSiloSelectionEnabled = MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||
if ($bIsSiloSelectionEnabled)
|
||||
{
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Shortcuts
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// 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.
|
||||
@@ -146,23 +146,18 @@ class DBBackup
|
||||
/**
|
||||
* Create a normalized backup name, depending on the current date/time and Database
|
||||
*
|
||||
* @param string|null $sNameSpec Name and path, eventually containing itop placeholders + time formatting following the strftime() format {@link https://www.php.net/manual/fr/function.strftime.php}
|
||||
* @param string $sNameSpec Name and path, eventually containing itop placeholders + time formatting following the strftime() format {@link https://www.php.net/manual/fr/function.strftime.php}
|
||||
* @param \DateTime|null $oDateTime Date time to use for the name
|
||||
*
|
||||
* @return ?string Name of the backup file WITHOUT the file extension (eg. `.tar.gz`)
|
||||
* @return string Name of the backup file WITHOUT the file extension (eg. `.tar.gz`)
|
||||
* @since 3.1.0 N°5279 Add $oDateTime parameter
|
||||
*/
|
||||
public function MakeName(?string $sNameSpec = null, ?DateTime $oDateTime = null)
|
||||
public function MakeName(string $sNameSpec = "__DB__-%Y-%m-%d", DateTime $oDateTime = null)
|
||||
{
|
||||
if ($oDateTime === null) {
|
||||
$oDateTime = new DateTime();
|
||||
}
|
||||
|
||||
//N°6640
|
||||
if ($sNameSpec === null) {
|
||||
$sNameSpec = "__DB__-%Y-%m-%d";
|
||||
}
|
||||
|
||||
$sFileName = $sNameSpec;
|
||||
$sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName);
|
||||
$sFileName = str_replace('__DB__', $this->sDBName, $sFileName);
|
||||
@@ -227,7 +222,7 @@ class DBBackup
|
||||
*
|
||||
* @param string $sSourceConfigFile
|
||||
* @param string $sTmpFolder
|
||||
* @param bool $bSkipSQLDumpForTesting
|
||||
* @param bool $bSkipSQLDumpForTesting
|
||||
*
|
||||
* @return array list of files to archive
|
||||
* @throws \Exception
|
||||
@@ -278,7 +273,7 @@ class DBBackup
|
||||
if(!file_exists(APPROOT.'/'.$sExtraFileOrDir)) {
|
||||
continue; // Ignore non-existing files
|
||||
}
|
||||
|
||||
|
||||
$sExtraFullPath = utils::RealPath(APPROOT.'/'.$sExtraFileOrDir, APPROOT);
|
||||
if ($sExtraFullPath === false)
|
||||
{
|
||||
@@ -363,6 +358,7 @@ class DBBackup
|
||||
|
||||
$sPortOption = self::GetMysqliCliSingleOption('port', $this->iDBPort);
|
||||
$sTlsOptions = self::GetMysqlCliTlsOptions($this->oConfig);
|
||||
$sProtocolOption = self::GetMysqlCliTransportOption($this->sDBHost);
|
||||
|
||||
$sMysqlVersion = CMDBSource::GetDBVersion();
|
||||
$bIsMysqlSupportUtf8mb4 = (version_compare($sMysqlVersion, self::MYSQL_VERSION_WITH_UTF8MB4_IN_PROGRAMS) === -1);
|
||||
@@ -383,8 +379,8 @@ EOF;
|
||||
|
||||
// Note: opt implicitely sets lock-tables... which cancels the benefit of single-transaction!
|
||||
// skip-lock-tables compensates and allows for writes during a backup
|
||||
$sCommand = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1";
|
||||
$sCommandDisplay = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables";
|
||||
$sCommand = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption $sProtocolOption --user=$sUser $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables 2>&1";
|
||||
$sCommandDisplay = "$sMySQLDump --defaults-extra-file=\"$sMySQLDumpCnfFile\" --opt --skip-lock-tables --default-character-set=".$sMysqldumpCharset." --add-drop-database --single-transaction --host=$sHost $sPortOption $sProtocolOption --user=xxxxx $sTlsOptions --result-file=$sTmpFileName $sDBName $sTables";
|
||||
|
||||
// Now run the command for real
|
||||
$this->LogInfo("backup: generate data file with command: $sCommandDisplay");
|
||||
@@ -472,8 +468,8 @@ EOF;
|
||||
if ($oMysqli->connect_errno)
|
||||
{
|
||||
$sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort;
|
||||
throw new BackupException("Cannot connect to the MySQL server '$sHost' (".$oMysqli->connect_errno.") ".$oMysqli->connect_error);
|
||||
}
|
||||
throw new MySQLException('Could not connect to the DB server '.$oMysqli->connect_errno.' (mysql errno: '.$oMysqli->connect_error, array('host' => $sHost, 'user' => $sUser));
|
||||
}
|
||||
if (!$oMysqli->select_db($this->sDBName))
|
||||
{
|
||||
throw new BackupException("The database '$this->sDBName' does not seem to exist");
|
||||
@@ -581,6 +577,28 @@ EOF;
|
||||
return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define if we should force a transport option
|
||||
*
|
||||
* @param string $sHost
|
||||
*
|
||||
* @return string .
|
||||
|
||||
* @since 2.7.9 3.0.4 3.1.1 N°6123
|
||||
*/
|
||||
public static function GetMysqlCliTransportOption(string $sHost)
|
||||
{
|
||||
$sTransportOptions = '';
|
||||
|
||||
/** N°6123 As we're using a --port option, if we use localhost as host,
|
||||
* MariaDB > 10.6 will implicitly change its protocol from socket to tcp and throw a warning **/
|
||||
if($sHost === 'localhost'){
|
||||
$sTransportOptions = '--protocol=tcp';
|
||||
}
|
||||
|
||||
return $sTransportOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string the command to launch mysqldump (without its params)
|
||||
*/
|
||||
|
||||
@@ -23,15 +23,15 @@ use Combodo\iTop\DesignElement;
|
||||
|
||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
require_once(APPROOT.'setup/parentmenunodecompiler.class.inc.php');
|
||||
require_once(APPROOT.'core/moduledesign.class.inc.php');
|
||||
|
||||
|
||||
class DOMFormatException extends Exception
|
||||
{
|
||||
/**
|
||||
* Overrides the Exception default constructor to automatically add informations about the concerned node (path and
|
||||
* line number)
|
||||
*
|
||||
*
|
||||
* @param string $message
|
||||
* @param $code
|
||||
* @param $previous
|
||||
@@ -49,7 +49,7 @@ class DOMFormatException extends Exception
|
||||
|
||||
/**
|
||||
* Compiler class
|
||||
*/
|
||||
*/
|
||||
class MFCompiler
|
||||
{
|
||||
const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR;
|
||||
@@ -356,7 +356,7 @@ class MFCompiler
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" of all modules
|
||||
@@ -368,16 +368,21 @@ class MFCompiler
|
||||
*/
|
||||
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
||||
{
|
||||
$aAllClasses = []; // flat list of classes
|
||||
$aModulesInfo = []; // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
$aAllClasses = array(); // flat list of classes
|
||||
$aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
|
||||
// Determine the target modules for the MENUS
|
||||
//
|
||||
$aMenuNodes = array();
|
||||
$aMenusByModule = array();
|
||||
foreach ($this->oFactory->GetNodes('menus/menu') as $oMenuNode)
|
||||
{
|
||||
$sMenuId = $oMenuNode->getAttribute('id');
|
||||
$aMenuNodes[$sMenuId] = $oMenuNode;
|
||||
|
||||
/**
|
||||
* @since 3.1 N°4762
|
||||
*/
|
||||
$oParentMenuNodeCompiler = new ParentMenuNodeCompiler($this);
|
||||
$oParentMenuNodeCompiler->LoadXmlMenus($this->oFactory);
|
||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||
$aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||
}
|
||||
|
||||
// Determine the target module (exactly one!) for USER RIGHTS
|
||||
// This used to be based solely on the module which created the user_rights node first
|
||||
@@ -424,7 +429,6 @@ class MFCompiler
|
||||
|
||||
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||
|
||||
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleName = $oModule->GetName();
|
||||
$sModuleVersion = $oModule->GetVersion();
|
||||
@@ -509,7 +513,7 @@ class MFCompiler
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($oParentMenuNodeCompiler->GetMenusByModule($sModuleName)))
|
||||
if (!array_key_exists($sModuleName, $aMenusByModule))
|
||||
{
|
||||
$this->Log("Found module without menus declared: $sModuleName");
|
||||
}
|
||||
@@ -529,19 +533,79 @@ class $sMenuCreationClass extends ModuleHandlerAPI
|
||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||
|
||||
EOF;
|
||||
|
||||
$oParentMenuNodeCompiler->CompileModuleMenus($oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
|
||||
// Preliminary: determine parent menus not defined within the current module
|
||||
$aMenusToLoad = array();
|
||||
$aParentMenus = array();
|
||||
foreach($aMenusByModule[$sModuleName] as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
// compute parent hierarchy
|
||||
$aParentIdHierarchy = [];
|
||||
while ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
array_unshift($aParentIdHierarchy, $sParent);
|
||||
$oMenuNode = $aMenuNodes[$sParent];
|
||||
}
|
||||
$aMenusToLoad = array_merge($aMenusToLoad, $aParentIdHierarchy);
|
||||
$aParentMenus = array_merge($aParentMenus, $aParentIdHierarchy);
|
||||
// Note: the order matters: the parents must be defined BEFORE
|
||||
$aMenusToLoad[] = $sMenuId;
|
||||
}
|
||||
$aMenusToLoad = array_unique($aMenusToLoad);
|
||||
$aMenuLinesForAll = array();
|
||||
$aMenuLinesForAdmins = array();
|
||||
$aAdminMenus = array();
|
||||
foreach($aMenusToLoad as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
if (is_null($oMenuNode))
|
||||
{
|
||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||
}
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup')
|
||||
{
|
||||
// Note: this algorithm is wrong
|
||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||
// 2 - check recursively that child nodes are not empty themselves
|
||||
// Future algorithm:
|
||||
// a- browse the modules and build the menu tree
|
||||
// b- browse the tree and blacklist empty menus
|
||||
// c- before compiling, discard if blacklisted
|
||||
if (!in_array($oMenuNode->getAttribute("id"), $aParentMenus))
|
||||
{
|
||||
// Discard empty menu groups
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
/** @var \iTopWebPage $oP */
|
||||
$aMenuLines = $this->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]))
|
||||
{
|
||||
$aMenuLinesForAdmins = array_merge($aMenuLinesForAdmins, $aMenuLines);
|
||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aMenuLinesForAll = array_merge($aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
}
|
||||
$sIndent = "\t\t";
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAll() as $sPHPLine)
|
||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||
}
|
||||
if (count($oParentMenuNodeCompiler->GetMenuLinesForAdmins()) > 0)
|
||||
if (count($aMenuLinesForAdmins) > 0)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||
$sCompiledCode .= $sIndent."{\n";
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAdmins() as $sPHPLine)
|
||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||
}
|
||||
@@ -573,7 +637,7 @@ EOF;
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
@@ -677,7 +741,7 @@ PHP;
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compile the portals
|
||||
/** @var \MFElement $oPortalsNode */
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
@@ -691,7 +755,7 @@ PHP;
|
||||
/** @var \MFElement $oParametersNode */
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['after'] as $aSnippet)
|
||||
@@ -725,7 +789,7 @@ PHP;
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
@@ -734,7 +798,7 @@ PHP;
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
@@ -742,14 +806,14 @@ EOF
|
||||
$sModulesInfo = str_replace("'".$sRelFinalTargetDir."/", "\$sCurrEnv.'/", $sModulesInfo);
|
||||
$sPHPFileContent .= "\nfunction GetModulesInfo()\n{\n\$sCurrEnv = 'env-'.utils::GetCurrentEnvironment();\nreturn ".$sModulesInfo.";\n}\n";
|
||||
file_put_contents($sPHPFile, $sPHPFileContent);
|
||||
|
||||
|
||||
} // DoCompile()
|
||||
|
||||
/**
|
||||
* Helper to form a valid ZList from the array built by GetNodeAsArrayOfItems()
|
||||
*
|
||||
* @param array $aItems
|
||||
*/
|
||||
*/
|
||||
protected function ArrayOfItemsToZList(&$aItems)
|
||||
{
|
||||
// Note: $aItems can be null in some cases so we have to protect it otherwise a PHP warning will be thrown during the foreach
|
||||
@@ -781,7 +845,7 @@ EOF
|
||||
* Helper to format the flags for an attribute, in a given state
|
||||
* @param object $oAttNode DOM node containing the information to build the flags
|
||||
* Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted)
|
||||
*/
|
||||
*/
|
||||
protected function FlagsToPHP($oAttNode)
|
||||
{
|
||||
static $aNodeAttributeToFlag = array(
|
||||
@@ -791,7 +855,7 @@ EOF
|
||||
'must_change' => 'OPT_ATT_MUSTCHANGE',
|
||||
'hidden' => 'OPT_ATT_HIDDEN',
|
||||
);
|
||||
|
||||
|
||||
$aFlags = array();
|
||||
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
|
||||
{
|
||||
@@ -803,7 +867,7 @@ EOF
|
||||
}
|
||||
if (empty($aFlags))
|
||||
{
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
}
|
||||
$sRes = implode(' | ', $aFlags);
|
||||
return $sRes;
|
||||
@@ -825,7 +889,7 @@ EOF
|
||||
'details' => 'LINKSET_TRACKING_DETAILS',
|
||||
'all' => 'LINKSET_TRACKING_ALL',
|
||||
);
|
||||
|
||||
|
||||
static $aXmlToPHP_Others = array(
|
||||
'none' => 'ATTRIBUTE_TRACKING_NONE',
|
||||
'all' => 'ATTRIBUTE_TRACKING_ALL',
|
||||
@@ -866,7 +930,7 @@ EOF
|
||||
'in_place' => 'LINKSET_EDITMODE_INPLACE',
|
||||
'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE',
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($sEditMode, $aXmlToPHP))
|
||||
{
|
||||
throw new DOMFormatException("Edit mode: unknown value '$sEditMode'");
|
||||
@@ -874,10 +938,10 @@ EOF
|
||||
return $aXmlToPHP[$sEditMode];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Format a path (file or url) as an absolute path or relative to the module or the app
|
||||
*/
|
||||
*/
|
||||
protected function PathToPHP($sPath, $sModuleRelativeDir, $bIsUrl = false)
|
||||
{
|
||||
if ($sPath == '')
|
||||
@@ -970,7 +1034,7 @@ EOF
|
||||
else
|
||||
{
|
||||
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1074,7 +1138,7 @@ EOF
|
||||
|
||||
/**
|
||||
* Adds quotes and escape characters
|
||||
*/
|
||||
*/
|
||||
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
|
||||
{
|
||||
if ($bSimpleQuotes)
|
||||
@@ -1181,7 +1245,7 @@ EOF
|
||||
$sScalar = (string)(int)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'float':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1193,7 +1257,7 @@ EOF
|
||||
$sScalar = (string)(float)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'bool':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1996,7 +2060,6 @@ EOF
|
||||
$this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
|
||||
$aParameters['depends_on'] = $sDependencies;
|
||||
} elseif ($sAttType == 'AttributeLinkedSet') {
|
||||
@@ -2659,7 +2722,7 @@ CSS;
|
||||
* @throws \DOMException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
protected function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
{
|
||||
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
|
||||
|
||||
@@ -2755,11 +2818,11 @@ CSS;
|
||||
case '1':
|
||||
$sSearchFormOpen = 'true';
|
||||
break;
|
||||
|
||||
|
||||
case '0':
|
||||
$sSearchFormOpen = 'false';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$sSearchFormOpen = 'true';
|
||||
}
|
||||
@@ -2848,7 +2911,7 @@ CSS;
|
||||
foreach($this->oFactory->ListFields($oClass) as $oField)
|
||||
{
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
if (($sAttType == 'AttributeExternalKey') || ($sAttType == 'AttributeHierarchicalKey'))
|
||||
{
|
||||
$sOnTargetDel = $oField->GetChildText('on_target_delete');
|
||||
@@ -2874,7 +2937,7 @@ CSS;
|
||||
$oClasses = $oGroup->GetUniqueElement('classes');
|
||||
foreach($oClasses->getElementsByTagName('class') as $oClass)
|
||||
{
|
||||
|
||||
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aClasses[] = $sClass;
|
||||
|
||||
@@ -2892,7 +2955,7 @@ CSS;
|
||||
$aProfiles[1] = array(
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Has the rights on everything (bypassing any control)',
|
||||
);
|
||||
);
|
||||
|
||||
$aGrants = array();
|
||||
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
|
||||
@@ -2924,7 +2987,7 @@ CSS;
|
||||
}
|
||||
$sGrant = $oAction->GetText();
|
||||
$bGrant = ($sGrant == 'allow');
|
||||
|
||||
|
||||
if ($sGroupId == '*')
|
||||
{
|
||||
$aGrantClasses = array('*');
|
||||
@@ -3163,7 +3226,7 @@ Dict::SetLanguagesList(
|
||||
$sLanguagesDump
|
||||
);
|
||||
EOF;
|
||||
|
||||
|
||||
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
|
||||
}
|
||||
|
||||
@@ -3200,7 +3263,7 @@ EOF;
|
||||
{
|
||||
throw new DOMFormatException('Could not find the file with ref '.$sFileId);
|
||||
}
|
||||
|
||||
|
||||
$sName = $oNodes->item(0)->GetChildText('name');
|
||||
$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
|
||||
$aPathInfo = pathinfo($sName);
|
||||
@@ -3214,7 +3277,7 @@ EOF;
|
||||
}
|
||||
$oParentNode = $oFileRef->parentNode;
|
||||
$oParentNode->removeChild($oFileRef);
|
||||
|
||||
|
||||
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
|
||||
$oParentNode->appendChild($oTextNode);
|
||||
}
|
||||
@@ -3295,7 +3358,7 @@ EOF;
|
||||
'utility_imports' => array(),
|
||||
'stylesheets' => array(),
|
||||
);
|
||||
|
||||
|
||||
if($oThemesCommonNodes !== null) {
|
||||
/** @var \DOMNodeList $oThemesCommonVariables */
|
||||
$oThemesCommonVariables = $oThemesCommonNodes->GetNodes('variables/variable');
|
||||
@@ -3303,7 +3366,7 @@ EOF;
|
||||
$sVariableId = $oVariable->getAttribute('id');
|
||||
$aThemesCommonParameters['variables'][$sVariableId] = $oVariable->GetText();
|
||||
}
|
||||
|
||||
|
||||
/** @var \DOMNodeList $oThemesCommonImports */
|
||||
$oThemesCommonImports = $oThemesCommonNodes->GetNodes('imports/import');
|
||||
foreach ($oThemesCommonImports as $oImport) {
|
||||
@@ -3317,7 +3380,7 @@ EOF;
|
||||
SetupLog::Warning('CompileThemes: Theme common has an import (#'.$sImportId.') without explicit xsi:type, it will be ignored. Check Datamodel XML Reference to fix it.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Stylesheets
|
||||
// - Manually added in the XML
|
||||
/** @var \DOMNodeList $oThemesCommonStylesheets */
|
||||
@@ -3382,7 +3445,7 @@ EOF;
|
||||
$aThemeParameters[$sThemeParameterName] = array_merge($aThemeParameter, $aThemesCommonParameters[$sThemeParameterName]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aThemes[$sThemeId] = [
|
||||
'theme_parameters' => $aThemeParameters,
|
||||
'precompiled_stylesheet' => $oTheme->GetChildText('precompiled_stylesheet', ''),
|
||||
@@ -3576,8 +3639,8 @@ EOF;
|
||||
{
|
||||
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
|
||||
}
|
||||
|
||||
// Compile themes
|
||||
|
||||
// Compile themes
|
||||
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -3618,11 +3681,11 @@ EOF;
|
||||
{
|
||||
$aPortalsConfig[$sPortalId]['deny'][] = $oProfile->getAttribute('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uasort($aPortalsConfig, array(get_class($this), 'SortOnRank'));
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Portal(s) definition(s) extracted from the XML definition at compile time\n";
|
||||
@@ -3665,7 +3728,7 @@ EOF;
|
||||
$oParamsReader = new MFParameters($oParams);
|
||||
$aParametersConfig[$sModuleId] = $oParamsReader->GetAll();
|
||||
}
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Modules parameters extracted from the XML definition at compile time\n";
|
||||
|
||||
@@ -892,7 +892,10 @@ class iTopDesignFormat
|
||||
$oNodeList = $oXPath->query("/itop_design/classes//class/fields/field/values/value");
|
||||
foreach ($oNodeList as $oNode) {
|
||||
$sCode = $oNode->textContent;
|
||||
$oNode->textContent = '';
|
||||
// N°6562 textContent is readonly, see https://www.php.net/manual/en/class.domnode.php#95545
|
||||
// $oNode->textContent = '';
|
||||
// N°6562 to update text node content we must use the node methods !
|
||||
$oNode->removeChild($oNode->firstChild);
|
||||
$oCodeNode = $oNode->ownerDocument->createElement("code", $sCode);
|
||||
$oNode->appendChild($oCodeNode);
|
||||
}
|
||||
@@ -982,7 +985,14 @@ class iTopDesignFormat
|
||||
if ($oStyleNode) {
|
||||
$this->DeleteNode($oStyleNode);
|
||||
}
|
||||
$oNode->textContent = $sCode;
|
||||
|
||||
// N°6562 textContent is readonly, see https://www.php.net/manual/en/class.domnode.php#95545
|
||||
// $oNode->textContent = $sCode;
|
||||
// N°6562 to update text node content we must use the node methods !
|
||||
// we are using DOMDocument::createTextNode instead of new DOMText because elements created using the constructor are read only
|
||||
// see https://www.php.net/manual/en/domelement.construct.php
|
||||
$oTextContentNode = $this->oDocument->createTextNode($sCode);
|
||||
$oNode->appendChild($oTextContentNode);
|
||||
}
|
||||
}
|
||||
// - Style
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @since 3.1 N°4762
|
||||
*/
|
||||
class ParentMenuNodeCompiler
|
||||
{
|
||||
const COMPILED = 1;
|
||||
const COMPILING = 2;
|
||||
|
||||
public static $bUseLegacyMenuCompilation = false;
|
||||
|
||||
/**
|
||||
* @var MFCompiler
|
||||
*/
|
||||
private $oMFCompiler;
|
||||
|
||||
/**
|
||||
* admin menus declaration lines: result of module menu compilation
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAdmins = [];
|
||||
|
||||
/**
|
||||
* non-admin menus declaration lines: result of module menu compilation
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAll = [];
|
||||
|
||||
/**
|
||||
* use to handle menu group compilation recurring algorithm
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuProcessStatus = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuNodes = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenusByModule = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenusToLoadByModule = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aParentMenusByModule = [];
|
||||
|
||||
/**
|
||||
* used by overall algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentMenuNodes = [];
|
||||
|
||||
/**
|
||||
* used by new algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentAdminMenus = [];
|
||||
|
||||
/**
|
||||
* used by overall algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentModuleRootDirs = [];
|
||||
|
||||
public function __construct(MFCompiler $oMFCompiler) {
|
||||
$this->oMFCompiler = $oMFCompiler;
|
||||
}
|
||||
|
||||
public static function UseLegacyMenuCompilation(){
|
||||
self::$bUseLegacyMenuCompilation = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ModelFactory $oFactory
|
||||
* Initialize menu nodes arrays
|
||||
* @return void
|
||||
*/
|
||||
public function LoadXmlMenus(\ModelFactory $oFactory) : void {
|
||||
foreach ($oFactory->GetNodes('menus/menu') as $oMenuNode) {
|
||||
$sMenuId = $oMenuNode->getAttribute('id');
|
||||
$this->aMenuNodes[$sMenuId] = $oMenuNode;
|
||||
|
||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||
$this->aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aModules
|
||||
* Initialize arrays related to parent/child menus
|
||||
* @return void
|
||||
*/
|
||||
public function LoadModuleMenuInfo($aModules) : void
|
||||
{
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleRootDir = $oModule->GetRootDir();
|
||||
$sModuleName = $oModule->GetName();
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||
$aMenusToLoad = [];
|
||||
$aParentMenus = [];
|
||||
|
||||
foreach ($this->aMenusByModule[$sModuleName] as $sMenuId) {
|
||||
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||
|
||||
if (self::$bUseLegacyMenuCompilation){
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
$aMenusToLoad[] = $sParent;
|
||||
$aParentMenus[] = $sParent;
|
||||
}
|
||||
} else {
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||
$this->aParentModuleRootDirs[$sMenuId] = $sModuleRootDir;
|
||||
}
|
||||
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
$aMenusToLoad[] = $sParent;
|
||||
$aParentMenus[] = $sParent;
|
||||
|
||||
$this->aParentModuleRootDirs[$sParent] = $sModuleRootDir;
|
||||
}
|
||||
|
||||
if (array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||
$this->aParentMenuNodes[$sMenuId] = $oMenuNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: the order matters: the parents must be defined BEFORE
|
||||
$aMenusToLoad[] = $sMenuId;
|
||||
}
|
||||
|
||||
$this->aMenusToLoadByModule[$sModuleName] = array_unique($aMenusToLoad);
|
||||
$this->aParentMenusByModule[$sModuleName] = array_unique($aParentMenus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" for one module at a time
|
||||
* @param \MFModule $oModule
|
||||
* @param string $sTempTargetDir
|
||||
* @param string $sFinalTargetDir
|
||||
* @param string $sRelativeDir
|
||||
* @param Page $oP
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function CompileModuleMenus(MFModule $oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||
{
|
||||
$this->aMenuLinesForAdmins = [];
|
||||
$this->aMenuLinesForAll = [];
|
||||
$aAdminMenus = [];
|
||||
|
||||
$sModuleRootDir = $oModule->GetRootDir();
|
||||
$sModuleName = $oModule->GetName();
|
||||
|
||||
$aParentMenus = $this->aParentMenusByModule[$sModuleName];
|
||||
foreach($this->aMenusToLoadByModule[$sModuleName] as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||
if (is_null($oMenuNode))
|
||||
{
|
||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||
}
|
||||
|
||||
if (self::$bUseLegacyMenuCompilation) {
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||
// Note: this algorithm is wrong
|
||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||
// 2 - check recursively that child nodes are not empty themselves
|
||||
// Future algorithm:
|
||||
// a- browse the modules and build the menu tree
|
||||
// b- browse the tree and blacklist empty menus
|
||||
// c- before compiling, discard if blacklisted
|
||||
if (! in_array($oMenuNode->getAttribute("id"), $aParentMenus)) {
|
||||
// Discard empty menu groups
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (array_key_exists($sMenuId, $this->aParentMenuNodes)) {
|
||||
// compile parent menus recursively
|
||||
$this->CompileParentMenuNode($sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//both new/legacy algo: compile leaf menu
|
||||
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]) || isset($this->aParentAdminMenus[$sParent]))
|
||||
{
|
||||
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform parent menu compilation including its ancestrors (recursively)
|
||||
* @param string $sMenuId
|
||||
* @param string $sTempTargetDir
|
||||
* @param string $sFinalTargetDir
|
||||
* @param string $sRelativeDir
|
||||
* @param Page $oP
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function CompileParentMenuNode(string $sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||
{
|
||||
$oMenuNode = $this->aParentMenuNodes[$sMenuId];
|
||||
$sStatus = array_key_exists($sMenuId, $this->aMenuProcessStatus) ? $this->aMenuProcessStatus[$sMenuId] : null;
|
||||
if ($sStatus === self::COMPILED){
|
||||
//node already processed before
|
||||
return;
|
||||
} else if ($sStatus === self::COMPILING){
|
||||
throw new \Exception("Cyclic dependency between parent menus ($sMenuId)");
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILING;
|
||||
|
||||
try {
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (! empty($sParent)){
|
||||
//compile parents before (even parent of parents ... recursively)
|
||||
$this->CompileParentMenuNode($sParent, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
|
||||
if (! array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||
throw new Exception("Failed to process parent menu '$sMenuId' that is referenced by a child but not defined");
|
||||
}
|
||||
$sModuleRootDir = $this->aParentModuleRootDirs[$sMenuId];
|
||||
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
} catch (DOMFormatException $e) {
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($this->aParentAdminMenus[$sParent])) {
|
||||
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||
$this->aParentAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
} else {
|
||||
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILED;
|
||||
}
|
||||
|
||||
public function GetMenusByModule(string $sModuleName) : ?array
|
||||
{
|
||||
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||
return $this->aMenusByModule[$sModuleName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAdmins(): array {
|
||||
return $this->aMenuLinesForAdmins;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAll(): array {
|
||||
return $this->aMenuLinesForAll;
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,13 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
public const UI_BLOCK_CLASS_NAME = DataTable::class;
|
||||
|
||||
/**
|
||||
* If inside an iTop object, you can use {@see cmdbAbstractObject::DisplaySet()}
|
||||
*
|
||||
* @api
|
||||
* @param \WebPage $oPage
|
||||
* @param string $sListId
|
||||
* @param \DBObjectSet $oSet
|
||||
* @param array $aExtraParams
|
||||
* @param array $aExtraParams See possible values in {@see self::RenderDataTable()}
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
|
||||
* @throws \ApplicationException
|
||||
@@ -84,11 +86,13 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* If inside an iTop object, you can use {@see cmdbAbstractObject::DisplaySet()}
|
||||
*
|
||||
* @api
|
||||
* @param \WebPage $oPage
|
||||
* @param string $sListId
|
||||
* @param DBObjectSet $oSet
|
||||
* @param array $aExtraParams
|
||||
* @param array $aExtraParams See possible values in {@see self::RenderDataTable()}
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
|
||||
* @throws \ArchivedObjectException
|
||||
@@ -117,7 +121,12 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
|
||||
* @param \WebPage $oPage
|
||||
* @param string $sListId
|
||||
* @param \DBObjectSet $oSet
|
||||
* @param array $aExtraParams
|
||||
* @param array $aExtraParams example keys used in this method :
|
||||
* - toolkit_menu = boolean
|
||||
* - surround_with_panel = boolean : if true adds the standard class panel (icon, title, ...)
|
||||
* - panel_title = string
|
||||
* - panel_title_is_html = boolean
|
||||
* - panel_icon = string : class icon (for example from {@see MetaModel::GetClassIcon()})
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Layout\UIContentBlock
|
||||
* @throws \ArchivedObjectException
|
||||
|
||||
@@ -49,16 +49,6 @@ class NewsroomMenuFactory
|
||||
return $oMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is any Newsroom provider configured
|
||||
* @return boolean
|
||||
*/
|
||||
public static function HasProviders()
|
||||
{
|
||||
$aProviders = MetaModel::EnumPlugins('iNewsroomProvider');
|
||||
return count($aProviders) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare parameters for the newsroom JS widget
|
||||
*
|
||||
@@ -110,4 +100,4 @@ class NewsroomMenuFactory
|
||||
);
|
||||
return $aParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ namespace Combodo\iTop\Application\UI\Base\Component\PopoverMenu;
|
||||
|
||||
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItem;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
|
||||
use Dict;
|
||||
use JSPopupMenuItem;
|
||||
@@ -57,68 +56,30 @@ class PopoverMenuFactory
|
||||
->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT)
|
||||
->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE);
|
||||
|
||||
$aUserMenuItems = [];
|
||||
|
||||
// Allowed portals
|
||||
$aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu();
|
||||
self::AddPopoverMenuItems($aAllowedPortalsItems, $aUserMenuItems);
|
||||
if (!empty($aAllowedPortalsItems)) {
|
||||
$oMenu->AddSection('allowed_portals')
|
||||
->SetItems('allowed_portals', $aAllowedPortalsItems);
|
||||
}
|
||||
|
||||
// User related pages
|
||||
self::AddPopoverMenuItems(static::PrepareUserRelatedItemsForUserMenu(), $aUserMenuItems);
|
||||
$oMenu->AddSection('user_related')
|
||||
->SetItems('user_related', static::PrepareUserRelatedItemsForUserMenu());
|
||||
|
||||
// API: iPopupMenuExtension::MENU_USER_ACTIONS
|
||||
$aAPIItems = static::PrepareAPIItemsForUserMenu($oMenu);
|
||||
self::AddPopoverMenuItems($aAPIItems, $aUserMenuItems);
|
||||
if (count($aAPIItems) > 0) {
|
||||
$oMenu->AddSection('popup_menu_extension-menu_user_actions')
|
||||
->SetItems('popup_menu_extension-menu_user_actions', $aAPIItems);
|
||||
}
|
||||
|
||||
// Misc links
|
||||
/*$oMenu->AddSection('misc')
|
||||
->SetItems('misc', static::PrepareMiscItemsForUserMenu());*/
|
||||
self::AddPopoverMenuItems(static::PrepareMiscItemsForUserMenu(), $aUserMenuItems);
|
||||
|
||||
self::SortPopoverMenuItems($aUserMenuItems);
|
||||
|
||||
$oMenu->AddSection('misc')
|
||||
->AddItems('misc', $aUserMenuItems);
|
||||
->SetItems('misc', static::PrepareMiscItemsForUserMenu());
|
||||
|
||||
return $oMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PopoverMenuItem[] $aPopoverMenuItem
|
||||
* @param PopoverMenuItem[] $aUserMenuItems
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function AddPopoverMenuItems(array $aPopoverMenuItem, array &$aUserMenuItems) : void {
|
||||
foreach ($aPopoverMenuItem as $oPopoverMenuItem){
|
||||
$aUserMenuItems[$oPopoverMenuItem->GetUID()] = $oPopoverMenuItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PopoverMenuItem[] $aPopoverMenuItem
|
||||
* @param PopoverMenuItem[] $aUserMenuItems
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function SortPopoverMenuItems(array &$aUserMenuItems) : void {
|
||||
$aSortedMenusFromConfig = MetaModel::GetConfig()->Get('navigation_menu.sorted_popup_user_menu_items');
|
||||
if (!is_array($aSortedMenusFromConfig) || empty($aSortedMenusFromConfig)){
|
||||
return;
|
||||
}
|
||||
|
||||
$aSortedMenus = [];
|
||||
foreach ($aSortedMenusFromConfig as $sMenuUID){
|
||||
if (array_key_exists($sMenuUID, $aUserMenuItems)){
|
||||
$aSortedMenus[]=$aUserMenuItems[$sMenuUID];
|
||||
unset($aUserMenuItems[$sMenuUID]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($aUserMenuItems as $oMenu){
|
||||
$aSortedMenus[]=$oMenu;
|
||||
}
|
||||
$aUserMenuItems = $aSortedMenus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the allowed portals items for the current user
|
||||
@@ -312,4 +273,4 @@ class PopoverMenuFactory
|
||||
|
||||
return $oMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ use MetaModel;
|
||||
use UIExtKeyWidget;
|
||||
use UserRights;
|
||||
use utils;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu\NewsroomMenuFactory;
|
||||
|
||||
/**
|
||||
* Class NavigationMenu
|
||||
@@ -275,7 +274,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
*/
|
||||
public function IsNewsroomEnabled(): bool
|
||||
{
|
||||
return (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders());
|
||||
return MetaModel::GetConfig()->Get('newsroom_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ class NavigationMenuFactory
|
||||
{
|
||||
|
||||
$oNewsroomMenu = null;
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders())
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled'))
|
||||
{
|
||||
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
|
||||
}
|
||||
@@ -57,4 +57,4 @@ class NavigationMenuFactory
|
||||
new ApplicationContext(), PopoverMenuFactory::MakeUserMenuForNavigationMenu(), $oNewsroomMenu, NavigationMenu::BLOCK_CODE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
namespace Combodo\iTop\Application\WelcomePopup;
|
||||
|
||||
use Dict;
|
||||
use AbstractWelcomePopup;
|
||||
|
||||
/**
|
||||
* Implementation of the "default" Welcome Popup message
|
||||
* @since 3.1.0
|
||||
*/
|
||||
class DefaultWelcomePopup extends AbstractWelcomePopup
|
||||
{
|
||||
public function GetMessages()
|
||||
{
|
||||
return [
|
||||
[
|
||||
// Replacement of the welcome popup message which
|
||||
// was hard-coded in the pages/UI.php
|
||||
'id' => '0001',
|
||||
'title' => Dict::S('UI:WelcomeMenu:Title'),
|
||||
'twig' => '/templates/pages/backoffice/welcome_popup/default_welcome_popup',
|
||||
'importance' => \iWelcomePopup::IMPORTANCE_HIGH,
|
||||
'parameters' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
<?php
|
||||
namespace Combodo\iTop\Application\WelcomePopup;
|
||||
use AttributeDateTime;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use LogChannels;
|
||||
use MetaModel;
|
||||
use UserRights;
|
||||
use WelcomePopupAcknowledge;
|
||||
use iWelcomePopup;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Handling of the messages displayed in the "Welcome Popup"
|
||||
* @since 3.1.0
|
||||
*
|
||||
*/
|
||||
class WelcomePopupService
|
||||
{
|
||||
private const PROVIDER_KEY_LENGTH = 128;
|
||||
/**
|
||||
* Array of acknowledged messages for the current user
|
||||
* @var string[]
|
||||
*/
|
||||
static $aAcknowledgedMessage = null;
|
||||
|
||||
/**
|
||||
* Array of "providers" of welcome popup messages
|
||||
* @var iWelcomePopup[]
|
||||
*/
|
||||
protected $aMessagesProviders = null;
|
||||
|
||||
/**
|
||||
* Get the list of messages to display in the Welcome popup dialog
|
||||
* @return string[][]
|
||||
*/
|
||||
public function GetMessages()
|
||||
{
|
||||
$this->LoadProviders();
|
||||
return $this->ProcessMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the messages to display from a list of iWelcomePopup instances
|
||||
* The messages are ordered by importance (CRITICAL first) then by ID
|
||||
* Invalid messages or acknowledged messages are removed from the list
|
||||
* @return array
|
||||
*/
|
||||
protected function ProcessMessages(): array
|
||||
{
|
||||
$this->LoadProviders();
|
||||
$aMessages = [];
|
||||
foreach($this->aMessagesProviders as $oProvider) {
|
||||
$aProviderMessages = $oProvider->GetMessages();
|
||||
if (count($aProviderMessages) === 0) {
|
||||
IssueLog::Debug('Empty list of messages for '.get_class($oProvider), LogChannels::CONSOLE);
|
||||
}
|
||||
foreach($aProviderMessages as $aMessage) {
|
||||
$aReasons = [];
|
||||
if (!$this->IsMessageValid($aMessage, $aReasons)) {
|
||||
IssueLog::Error('Invalid structure returned by '.get_class($oProvider).'::GetMessages()', LogChannels::CONSOLE, $aReasons);
|
||||
continue; // Fail silently
|
||||
}
|
||||
$sUUID = $this->MakeStringFitIn(get_class($oProvider), static::PROVIDER_KEY_LENGTH).'::'.$aMessage['id'];
|
||||
$aMessage['uuid'] = $sUUID;
|
||||
$aMessages[] = $aMessage;
|
||||
}
|
||||
}
|
||||
// Filter the acknowledged messages AFTER getting all messages
|
||||
// This allows for "replacing" a message (from another provider for example)
|
||||
// by automatically acknowledging it when called in GetMessages()
|
||||
foreach($aMessages as $key => $aMessage) {
|
||||
if ($this->IsMessageAcknowledged($aMessage['uuid'])) {
|
||||
IssueLog::Debug('Ignoring already acknowledged message '.$aMessage['uuid'], LogChannels::CONSOLE);
|
||||
unset($aMessages[$key]);
|
||||
}
|
||||
}
|
||||
usort($aMessages, array(get_class($this), 'SortOnImportance'));
|
||||
return $aMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for usort to compare two items based on their 'importance' field
|
||||
* @param string[] $aItem1
|
||||
* @param string[] $aItem2
|
||||
* @return int
|
||||
*/
|
||||
public static function SortOnImportance($aItem1, $aItem2): int
|
||||
{
|
||||
if ($aItem1['importance'] === $aItem2['importance']) {
|
||||
return strcmp($aItem1['id'], $aItem2['id']);
|
||||
}
|
||||
return ($aItem1['importance'] < $aItem2['importance']) ? -1 : 1;
|
||||
}
|
||||
|
||||
public function AcknowledgeMessage(string $sMessageUUID): void
|
||||
{
|
||||
$this->LoadProviders();
|
||||
$oAcknowledge = MetaModel::NewObject(WelcomePopupAcknowledge::class, [
|
||||
'message_uuid' => $sMessageUUID,
|
||||
'acknowledge_date' => date(AttributeDateTime::GetSQLFormat()),
|
||||
'user_id' => UserRights::GetConnectedUserId(),
|
||||
]);
|
||||
try {
|
||||
$oAcknowledge->DBInsert();
|
||||
$oProvider = $this->GetProviderByUUID($sMessageUUID);
|
||||
if (static::$aAcknowledgedMessage !== null) {
|
||||
static::$aAcknowledgedMessage[] = $sMessageUUID; // Update the cache
|
||||
}
|
||||
// Notify the provider of the message
|
||||
$sMessageId = substr($sMessageUUID, strpos($sMessageUUID, '::')+2);
|
||||
if ($oProvider !== null) {
|
||||
$oProvider->AcknowledgeMessage($sMessageId);
|
||||
}
|
||||
} catch(Exception $e) {
|
||||
IssueLog::Error("Failed to acknowledge the message $sMessageUUID for user ".UserRights::GetConnectedUserId().". Reason: ".$e->getMessage(), LogChannels::CONSOLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the provider of messages, decoupled from the constructor for testability
|
||||
*/
|
||||
protected function LoadProviders(): void
|
||||
{
|
||||
if ($this->aMessagesProviders !== null) return;
|
||||
|
||||
$aProviders = [];
|
||||
$aProviderClasses = utils::GetClassesForInterface(iWelcomePopup::class, '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]'));
|
||||
foreach($aProviderClasses as $sProviderClass) {
|
||||
$aProviders[] = new $sProviderClass();
|
||||
}
|
||||
$this->SetMessagesProviders($aProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given message was acknowledged by the current user
|
||||
* @param string $sMessageId
|
||||
* @return bool
|
||||
*/
|
||||
protected function IsMessageAcknowledged(string $sMessageUUID): bool
|
||||
{
|
||||
$iUserId = UserRights::GetConnectedUserId();
|
||||
if (static::$aAcknowledgedMessage === null) {
|
||||
|
||||
$oSearch = new DBObjectSearch(WelcomePopupAcknowledge::class);
|
||||
$oSearch->AddCondition('user_id', $iUserId);
|
||||
$oSet = new DBObjectSet($oSearch);
|
||||
$aAcknowledgedMessages = $oSet->GetColumnAsArray('message_uuid');
|
||||
$this->SetAcknowledgedMessagesCache($aAcknowledgedMessages);
|
||||
}
|
||||
return in_array($sMessageUUID, static::$aAcknowledgedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache of acknowledged messages (useful for testing)
|
||||
* @param array $aAcknowledgedMessages
|
||||
*/
|
||||
protected function SetAcknowledgedMessagesCache(array $aAcknowledgedMessages): void
|
||||
{
|
||||
static::$aAcknowledgedMessage = $aAcknowledgedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cache of welcome popup message providers (useful for testing)
|
||||
* @param iWelcomePopup[] $aMessagesProviders
|
||||
*/
|
||||
protected function SetMessagesProviders(array $aMessagesProviders): void
|
||||
{
|
||||
$this->aMessagesProviders = $aMessagesProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the provider associated with a message
|
||||
* @param string $sMessageUUID
|
||||
* @return iWelcomePopup|NULL
|
||||
*/
|
||||
protected function GetProviderByUUID(string $sMessageUUID): ?iWelcomePopup
|
||||
{
|
||||
$this->LoadProviders();
|
||||
$sProviderKey = substr($sMessageUUID, 0, strpos($sMessageUUID, '::'));
|
||||
foreach($this->aMessagesProviders as $oProvider) {
|
||||
if ($this->MakeStringFitIn(get_class($oProvider), static::PROVIDER_KEY_LENGTH) === $sProviderKey) {
|
||||
return $oProvider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the structure of a given message is valid by checking
|
||||
* all its mandatory elements
|
||||
* @param string[] $aMessage
|
||||
* @param string[] $aReasons
|
||||
* @return bool
|
||||
*/
|
||||
protected function IsMessageValid($aMessage, array &$aReasons): bool
|
||||
{
|
||||
if (!is_array($aMessage)) {
|
||||
$aReasons[] = 'GetMessage() must return an array of arrays.';
|
||||
return false; // Stop checking immediately
|
||||
}
|
||||
$bRet = true;
|
||||
foreach(['id', 'importance', 'title'] as $sKey) {
|
||||
if (!array_key_exists($sKey, $aMessage)) {
|
||||
$aReasons[] = "Field '$sKey' missing from the message structure.";
|
||||
$bRet = false;
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('html', $aMessage) && !array_key_exists('twig', $aMessage)) {
|
||||
$aReasons[] = "Message structure must contain either a field 'html' or a field 'twig'.";
|
||||
$bRet = false;
|
||||
}
|
||||
return $bRet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the given string (if needed) but preserving its uniqueness
|
||||
* @param string $sProviderClass
|
||||
* @param int $iLengthLimit
|
||||
* @return string
|
||||
*/
|
||||
protected function MakeStringFitIn(string $sProviderClass, int $iLengthLimit): string
|
||||
{
|
||||
if(mb_strlen($sProviderClass) <= $iLengthLimit) {
|
||||
return $sProviderClass;
|
||||
}
|
||||
// Truncate the string to $iLimitLength and replace the first carahcters with the MD5 of the complete string
|
||||
$sMD5 = md5($sProviderClass, false);
|
||||
return $sMD5.'-'.mb_substr($sProviderClass, -($iLengthLimit - strlen($sMD5) - 1)); // strlen is OK on the MD5 string, and '-' is not allowed in a class name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
namespace Combodo\iTop\Controller;
|
||||
|
||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Simple controller to acknowledge (via Ajax) welcome popup messages
|
||||
* @since 3.1.0
|
||||
*
|
||||
*/
|
||||
class WelcomePopupController
|
||||
{
|
||||
/**
|
||||
* Operation: welcome_popup.acknowledge_message
|
||||
*/
|
||||
public function AcknowledgeMessage(): void
|
||||
{
|
||||
$oService = new WelcomePopupService();
|
||||
$sMessageUUID = utils::ReadPostedParam('message_uuid', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$oService->AcknowledgeMessage($sMessageUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class OAuthClientProviderFactory
|
||||
* @return AccessTokenInterface
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
|
||||
*/
|
||||
public static function GetAccessTokenFromCode(OAuthClient $oOAuthClient, $sCode)
|
||||
{
|
||||
@@ -109,7 +110,7 @@ class OAuthClientProviderFactory
|
||||
/**
|
||||
* @param \DBObject $oOAuthClient
|
||||
*
|
||||
* @return mixed
|
||||
* @return OAuthClientProviderAbstract
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
|
||||
125
sources/Core/Kpi/KpiLogData.php
Normal file
125
sources/Core/Kpi/KpiLogData.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Core\Kpi;
|
||||
|
||||
class KpiLogData
|
||||
{
|
||||
const TYPE_REPORT = 'report';
|
||||
const TYPE_STATS = 'stats';
|
||||
const TYPE_REQUEST = 'request';
|
||||
|
||||
/** @var string */
|
||||
public $sType;
|
||||
/** @var string */
|
||||
public $sOperation;
|
||||
/** @var string */
|
||||
public $sArguments;
|
||||
/** @var float */
|
||||
public $fStartTime;
|
||||
/** @var float */
|
||||
public $fStopTime;
|
||||
/** @var string */
|
||||
public $sExtension;
|
||||
/** @var int */
|
||||
public $iInitialMemory;
|
||||
/** @var int */
|
||||
public $iCurrentMemory;
|
||||
/** @var int */
|
||||
public $iPeakMemory;
|
||||
/** @var array */
|
||||
public $aData;
|
||||
|
||||
/**
|
||||
* @param string $sType
|
||||
* @param string $sOperation
|
||||
* @param string $sArguments
|
||||
* @param float $fStartTime
|
||||
* @param float $fStopTime
|
||||
* @param string $sExtension
|
||||
* @param int $iInitialMemory
|
||||
* @param int $iCurrentMemory
|
||||
* @param array $aData
|
||||
*/
|
||||
public function __construct($sType, $sOperation, $sArguments, $fStartTime, $fStopTime, $sExtension, $iInitialMemory = 0, $iCurrentMemory = 0, $iPeakMemory = 0, $aData = [])
|
||||
{
|
||||
$this->sType = $sType;
|
||||
$this->sOperation = $sOperation;
|
||||
$this->sArguments = @iconv(mb_detect_encoding($sArguments, mb_detect_order(), true), 'UTF-8', $sArguments);
|
||||
$this->fStartTime = $fStartTime;
|
||||
$this->fStopTime = $fStopTime;
|
||||
$this->sExtension = $sExtension;
|
||||
$this->iInitialMemory = $iInitialMemory;
|
||||
$this->iCurrentMemory = $iCurrentMemory;
|
||||
$this->iPeakMemory = $iPeakMemory;
|
||||
$this->aData = $aData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CSV Header
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function GetCSVHeader()
|
||||
{
|
||||
return "Type,Operation,Arguments,StartTime,StopTime,Duration,Extension,InitialMemory,CurrentMemory,PeakMemory";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CSV line for the values
|
||||
* @return string
|
||||
*/
|
||||
public function GetCSV()
|
||||
{
|
||||
$fDuration = sprintf('%01.4f', $this->fStopTime - $this->fStartTime);
|
||||
$sType = $this->RemoveQuotes($this->sType);
|
||||
$sOperation = $this->RemoveQuotes($this->sOperation);
|
||||
$sArguments = $this->RemoveQuotes($this->sArguments);
|
||||
$sExtension = $this->RemoveQuotes($this->sExtension);
|
||||
return "\"$sType\",\"$sOperation\",\"$sArguments\",$this->fStartTime,$this->fStopTime,$fDuration,\"$sExtension\",$this->iInitialMemory,$this->iCurrentMemory,$this->iPeakMemory";
|
||||
}
|
||||
|
||||
private function RemoveQuotes(string $sEntry): string
|
||||
{
|
||||
return str_replace('"', "'", $sEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\Core\Kpi\KpiLogData $oOther
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function Compare(KpiLogData $oOther): float
|
||||
{
|
||||
if ($oOther->fStartTime > $this->fStartTime) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function Contains(KpiLogData $oOther): bool
|
||||
{
|
||||
if ($oOther->fStartTime < $this->fStartTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($oOther->fStartTime > $this->fStopTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return "$this->sType:$this->sOperation:$this->sArguments";
|
||||
}
|
||||
|
||||
public function GetUUID(): string
|
||||
{
|
||||
return sha1($this->__toString());
|
||||
}
|
||||
}
|
||||
@@ -320,12 +320,19 @@ EOF
|
||||
if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '')
|
||||
{
|
||||
// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
|
||||
$oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
|
||||
$oFieldValue = MetaModel::GetObjectWithArchive($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
|
||||
$sFieldHtmlValue = $oFieldValue->GetName();
|
||||
$sFieldUrl = ApplicationContext::MakeObjectUrl($sFieldValueClass, $this->oField->GetCurrentValue());
|
||||
if(!empty($sFieldUrl))
|
||||
if($oFieldValue->IsArchived())
|
||||
{
|
||||
$sFieldHtmlValue = '<a href="'.$sFieldUrl.'" data-toggle="itop-portal-modal">'.$sFieldHtmlValue.'</a>';
|
||||
$sFieldHtmlValue = '<span class="text_decoration"><span class="fas fa-archive"></span></span>' . $sFieldHtmlValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sFieldUrl = ApplicationContext::MakeObjectUrl($sFieldValueClass, $this->oField->GetCurrentValue());
|
||||
if (!empty($sFieldUrl))
|
||||
{
|
||||
$sFieldHtmlValue = '<a href="' . $sFieldUrl . '" data-toggle="itop-portal-modal">' . $sFieldHtmlValue . '</a>';
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
214
sources/Service/Module/ModuleService.php
Normal file
214
sources/Service/Module/ModuleService.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Service\Module;
|
||||
|
||||
use MetaModel;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use utils;
|
||||
|
||||
class ModuleService
|
||||
{
|
||||
/** @var ModuleService */
|
||||
private static $oInstance;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function GetInstance(): ModuleService
|
||||
{
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new ModuleService();
|
||||
}
|
||||
|
||||
return static::$oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a "signature" of the method of an extension in the form of: "[module-name] class::method()"
|
||||
*
|
||||
* @param object|string $object Object or class
|
||||
* @param string $sMethod
|
||||
*
|
||||
* @return string
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function GetModuleMethodSignature($object, string $sMethod): string
|
||||
{
|
||||
$sSignature = '';
|
||||
$oReflectionMethod = new ReflectionMethod($object, $sMethod);
|
||||
$oReflectionClass = $oReflectionMethod->getDeclaringClass();
|
||||
$sExtension = $this->GetModuleNameFromObject($oReflectionClass->getName());
|
||||
if (strlen($sExtension) !== 0) {
|
||||
$sSignature .= '['.$sExtension.'] ';
|
||||
}
|
||||
$sSignature .= $oReflectionClass->getShortName().'::'.$sMethod.'()';
|
||||
|
||||
return $sSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module name from an object or class
|
||||
*
|
||||
* @param object|string $object
|
||||
*
|
||||
* @return string
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function GetModuleNameFromObject($object): string
|
||||
{
|
||||
$oReflectionClass = new ReflectionClass($object);
|
||||
$sPath = str_replace('\\', '/', $oReflectionClass->getFileName());
|
||||
$sPattern = str_replace('\\', '/', '@'.APPROOT.'env-'.utils::GetCurrentEnvironment()).'/(?<ext>.+)/@U';
|
||||
if (preg_match($sPattern, $sPath, $aMatches) !== false) {
|
||||
if (isset($aMatches['ext'])) {
|
||||
return $aMatches['ext'];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* **Warning** : returned result can be invalid as we're using backtrace to find the module dir name
|
||||
*
|
||||
* @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
|
||||
*
|
||||
* @return string the relative (to MODULESROOT) path of the root directory of the module containing the file where the call to
|
||||
* this function is made
|
||||
* or an empty string if no such module is found (or not called within a module file)
|
||||
*
|
||||
* @uses \debug_backtrace()
|
||||
*/
|
||||
public function GetCurrentModuleDir(int $iCallDepth): string
|
||||
{
|
||||
$sCurrentModuleDir = '';
|
||||
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
|
||||
|
||||
foreach(GetModulesInfo() as $sModuleName => $aInfo)
|
||||
{
|
||||
if ($aInfo['root_dir'] !== '')
|
||||
{
|
||||
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
|
||||
|
||||
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
|
||||
{
|
||||
$sCurrentModuleDir = basename($sRootDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sCurrentModuleDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Warning** : as this method uses {@see GetCurrentModuleDir} it produces hazardous results.
|
||||
* You should better uses directly {@see GetAbsoluteUrlModulesRoot} and add the module dir name yourself ! See N°4573
|
||||
*
|
||||
* @return string the base URL for all files in the current module from which this method is called
|
||||
* or an empty string if no such module is found (or not called within a module file)
|
||||
* @throws \Exception
|
||||
*
|
||||
* @uses GetCurrentModuleDir
|
||||
*/
|
||||
public function GetCurrentModuleUrl(int $iCallDepth = 0): string
|
||||
{
|
||||
$sDir = $this->GetCurrentModuleDir(1 + $iCallDepth);
|
||||
if ( $sDir !== '')
|
||||
{
|
||||
return utils::GetAbsoluteUrlModulesRoot().'/'.$sDir;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sProperty The name of the property to retrieve
|
||||
* @param mixed $defaultValue
|
||||
*
|
||||
* @return mixed the value of a given setting for the current module
|
||||
*/
|
||||
public function GetCurrentModuleSetting(string $sProperty, $defaultValue = null)
|
||||
{
|
||||
$sModuleName = $this->GetCurrentModuleName(1);
|
||||
return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sModuleName
|
||||
*
|
||||
* @return string|NULL compiled version of a given module, as it was seen by the compiler
|
||||
*/
|
||||
public function GetCompiledModuleVersion(string $sModuleName): ?string
|
||||
{
|
||||
$aModulesInfo = GetModulesInfo();
|
||||
if (array_key_exists($sModuleName, $aModulesInfo))
|
||||
{
|
||||
return $aModulesInfo[$sModuleName]['version'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the module containing the file where the call to this function is made
|
||||
* or an empty string if no such module is found (or not called within a module file)
|
||||
*
|
||||
* @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetCurrentModuleName(int $iCallDepth = 0): string
|
||||
{
|
||||
$sCurrentModuleName = '';
|
||||
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
|
||||
|
||||
return $this->GetModuleNameFromPath($sCallerFile);
|
||||
}
|
||||
|
||||
private function GetModuleNameFromPath($sPath)
|
||||
{
|
||||
foreach (GetModulesInfo() as $sModuleName => $aInfo) {
|
||||
if ($aInfo['root_dir'] !== '') {
|
||||
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
|
||||
if (substr($sPath, 0, strlen($sRootDir)) === $sRootDir) {
|
||||
|
||||
return $sModuleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extension code from the call stack.
|
||||
* Scan the call stack until a module is found.
|
||||
*
|
||||
* @param int $iLevelsToIgnore
|
||||
*
|
||||
* @return string module name
|
||||
*/
|
||||
public function GetModuleNameFromCallStack(int $iLevelsToIgnore = 0): string
|
||||
{
|
||||
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$aCallStack = array_slice($aCallStack, $iLevelsToIgnore);
|
||||
|
||||
foreach ($aCallStack as $aCallInfo) {
|
||||
$sFile = realpath(empty($aCallInfo['file']) ? '' : $aCallInfo['file']);
|
||||
|
||||
$sModuleName = $this->GetModuleNameFromPath($sFile);
|
||||
if (strlen($sModuleName) > 0) {
|
||||
return $sModuleName;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2162,7 +2162,9 @@ class SynchroReplica extends DBObject implements iDisplay
|
||||
// it will be deleted by the mean of a trigger too
|
||||
protected function DBDeleteSingleObject()
|
||||
{
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->OnDelete();
|
||||
$oKPI->ComputeStatsForExtension($this, 'OnDelete');
|
||||
|
||||
if (!MetaModel::DBIsReadOnly())
|
||||
{
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="ibo-welcome-popup--columns">
|
||||
<div class="ibo-welcome-popup--image ibo-svg-illustration--container">
|
||||
{{ source("images/illustrations/undraw_relaunch_day.svg") }}
|
||||
</div>
|
||||
<div class="ibo-welcome-popup--text">
|
||||
<div>
|
||||
{{ 'UI:WelcomeMenu:Text'| dict_s|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +1,14 @@
|
||||
<div id="welcome_popup_dialog" class="ibo-welcome-popup--dialog ibo-is-hidden">
|
||||
<div class="ibo-welcome-popup--content">
|
||||
{% for message in messages %}
|
||||
<div class="ibo-welcome-popup--message {% if not loop.first %}ibo-is-hidden{% endif %}" data-message-uuid="{{ message.uuid }}" data_role="welcome-popup-title" data-title="{{ message.title }}">
|
||||
{% if message.twig is defined %}
|
||||
{{ include([message.twig ~ '.html.twig', message.twig ~ '.twig', message.twig], message.parameters ?? {}, sandboxed = true) }}
|
||||
{% else %}
|
||||
{{ message.html|raw }}
|
||||
{% endif %}
|
||||
<div class="ibo-welcome-popup--button" data-message-uuid="{{ message.uuid }}">
|
||||
{% UIButton ForPrimaryAction{'sLabel':'UI:WelcomePopup:Button:Acknowledge'|dict_s, 'bIsSubmit': false } %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="ibo-welcome-popup--indicators">
|
||||
{% if messages|length > 1 %}
|
||||
{% for message in messages %}
|
||||
<span class="ibo-welcome-popup--indicator {% if loop.first %}ibo-welcome-popup--active{% endif %}" data-message-uuid="{{ message.uuid }}"></span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="welcome_popup">
|
||||
<div class="ibo-welcome-popup--image ibo-svg-illustration--container">
|
||||
{{ source("images/illustrations/undraw_relaunch_day.svg") }}
|
||||
</div>
|
||||
<div class="ibo-welcome-popup--text">
|
||||
<div>
|
||||
{{ 'UI:WelcomeMenu:Text'| dict_s|raw }}
|
||||
</div>
|
||||
<div class="ibo-welcome-popup--text--options">
|
||||
<input type="checkbox" checked id="display_welcome_popup"/><label for="display_welcome_popup">{{'UI:DisplayThisMessageAtStartup'| dict_s}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
$('#welcome_popup_dialog').removeClass('ibo-is-hidden');
|
||||
$('#welcome_popup_dialog').dialog({
|
||||
modal: true,
|
||||
width: '60%',
|
||||
autoOpen: true,
|
||||
title: $('div[data_role=welcome-popup-title]').first().attr('data-title'),
|
||||
close: function() { $('#welcome_popup_dialog').remove(); }
|
||||
});
|
||||
$('.ui-widget-overlay').click(function() { $('#welcome_popup_dialog').dialog('close'); } );
|
||||
$('.ibo-welcome-popup--indicator').click(function() {
|
||||
const id = $(this).attr('data-message-uuid');
|
||||
const escaped_id = id.replace(/\\/g, '\\\\'); // All backslashes must be doubled in a jQuery selector
|
||||
const new_title = $('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').attr('data-title');
|
||||
$('.ibo-welcome-popup--message').addClass('ibo-is-hidden');
|
||||
$('.ibo-welcome-popup--indicator').removeClass('ibo-welcome-popup--active');
|
||||
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').removeClass('ibo-is-hidden');
|
||||
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').addClass('ibo-welcome-popup--active');
|
||||
$('#welcome_popup_dialog').dialog('option', 'title', new_title);
|
||||
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"] button').focus();
|
||||
});
|
||||
$('.ibo-welcome-popup--button').click('button', function() {
|
||||
const id = $(this).attr('data-message-uuid');
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'welcome_popup_acknowledge_message', message_uuid: id});
|
||||
const escaped_id = id.replace(/\\/g, '\\\\');; // All backslashes must be doubled in a jQuery selector
|
||||
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').remove();
|
||||
if($('.ibo-welcome-popup--message').length == 0) {
|
||||
// Last message, close the dialog
|
||||
$('#welcome_popup_dialog').dialog('close');
|
||||
} else {
|
||||
// Move the active state to the next message
|
||||
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').siblings().first().trigger('click');
|
||||
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').remove();
|
||||
if ($('.ibo-welcome-popup--indicator').length == 1) {
|
||||
// Last indicator, remove it
|
||||
$('.ibo-welcome-popup--indicator').remove();
|
||||
}
|
||||
}
|
||||
$('#welcome_popup').dialog( { width:'60%', height: 'auto', title: '{{ 'UI:WelcomeMenu:Title'|dict_s }}', autoOpen: true, modal:true,
|
||||
close: function() {
|
||||
var bDisplay = $('#display_welcome_popup:checked').length;
|
||||
SetUserPreference('welcome_popup', bDisplay, true);
|
||||
},
|
||||
buttons: [{
|
||||
text: "{{ 'UI:Button:Ok'|dict_s }}", click: function() {
|
||||
$(this).dialog( "close" ); $(this).remove();
|
||||
}}],
|
||||
});
|
||||
if ($('#welcome_popup').height() > ($(window).height()-70))
|
||||
{
|
||||
$('#welcome_popup').height($(window).height()-70);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,118 @@
|
||||
# PHP unitary tests
|
||||
|
||||
## Where should I add my test?
|
||||
|
||||
- Covers an iTop PHP class or method?
|
||||
- Most likely in "unitary-tests".
|
||||
- Covers the consistency of some data through the app?
|
||||
- Most likely in "integration-tests".
|
||||
- Most likely in "integration-tests".
|
||||
|
||||
## How do I make sure that my tests are efficient?
|
||||
|
||||
|
||||
### Derive from the relevant test class
|
||||
|
||||
Whenever possible keep it the most simple, hence you should first
|
||||
attempt to derive from `TestCase`.
|
||||
|
||||
Then, you might need to derive from `ItopTestCase`.
|
||||
|
||||
Finally, as a last resort, you will use `ItopDataTestCase`.
|
||||
|
||||
### Determine the most relevant isolation configuration
|
||||
|
||||
Should you have opted for `ItopDataTestCase`, then you will have to follow these steps:
|
||||
|
||||
1) Build you test class until it is successfull, without process isolation.
|
||||
2) Run the whole test suite [unitary-tests](unitary-tests)
|
||||
3) If a false-positive appears, then you will start troubleshooting. One advise: be positive!
|
||||
|
||||
### Leave the place clean
|
||||
|
||||
To check your code against polluting coding patterns, run the test [integration-tests/DetectStaticPollutionTest.php](integration-tests/DetectStaticPollutionTest.php)
|
||||
It will tell you if something is wrong, either in your code, or anywhere else in the tests.
|
||||
Fortunately, it will give you an alternative.
|
||||
|
||||
Detected patterns:
|
||||
* ContextTag::addTag()
|
||||
* EventService::RegisterListener()
|
||||
* Dict::Add()
|
||||
|
||||
|
||||
By the way, some patterns do not pollute, because they are handled by the test framework:
|
||||
* Configuration : automatically reset after test class execution
|
||||
* UserRights : a logoff is performed after each test execution
|
||||
* Dict::SetUserLanguage: the user language is reset after each test execution
|
||||
|
||||
See also `@beforeClass` and `@afterClass` to handle cleanup.
|
||||
|
||||
If you can't, then ok you will have to isolate it!
|
||||
|
||||
## Tips
|
||||
### Memory limit
|
||||
|
||||
As the tests are run in the same process, memory usage
|
||||
may become an issue as soon as tests are all executed at once.
|
||||
|
||||
Fix that in the XML configuration in the PHP section
|
||||
```xml
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
```
|
||||
|
||||
### Understand tests interactions
|
||||
|
||||
With PHPStorm, select two tests, right click to get the context menu, then `run`.
|
||||
|
||||
You will have both tests executed and you will be able to figure out if the first one has an impact on the second one.
|
||||
|
||||
### About process isolation
|
||||
#### Isolation with PHPUnit
|
||||
|
||||
By default, tests are run in a single process launched by PHPUnit.
|
||||
|
||||
If process isolation is configured for some tests, then those tests
|
||||
will be executed in a separate process. The main process will
|
||||
continue executing non isolated tests.
|
||||
|
||||
#### Cost of isolation
|
||||
|
||||
The cost of isolating a very basic `TestCase` is approximately 4 ms.
|
||||
|
||||
The cost of isolating an `ItopDataTestCase` is approximately 800 ms.
|
||||
|
||||
### Isolation within iTop
|
||||
|
||||
#### At the test level (preferred)
|
||||
Add annotation `@runInSeparateProcess`
|
||||
Each and every test case will run in a separate
|
||||
process.
|
||||
|
||||
#### At the test class level
|
||||
Add annotation `@runTestsInSeparateProcesses`
|
||||
Each and every test case in the class will run in a separate
|
||||
process.
|
||||
|
||||
#### Globally (never do that)
|
||||
Set it into [phpunit.xml.dist](phpunit.xml.dist)
|
||||
|
||||
### Further enhancements
|
||||
The annotation [`@runClassInSeparateProcess`](https://docs.phpunit.de/en/10.0/attributes.html?highlight=runclassinseparateprocess#runclassinseparateprocess) is supposed to do the perfect job, but it is buggy [(See Issue 5230)](https://github.com/sebastianbergmann/phpunit/issues/5230) and it has
|
||||
the exact same effect as `@runTestsInSeparateProcesses`.
|
||||
|
||||
Note : this option is documented only in the [attributes part of the documentation](https://docs.phpunit.de/en/10.0/attributes.html).
|
||||
|
||||
### Traps
|
||||
#### When it is a matter of stars
|
||||
```php
|
||||
/*
|
||||
* @runTestsInSeparateProcesses
|
||||
```
|
||||
This won't work because the comment MUST start with `/**` (two stars) to be considerer by PHPUnit.
|
||||
|
||||
#### SetupBeforeClass called more often than expected
|
||||
|
||||
`setupBeforeClass` is called once for the class **in a given process**.
|
||||
|
||||
Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`.
|
||||
|
||||
This has been proven with [`runClassInSeparateProcessTest.php`](experiments/runClassInSeparateProcessTest.php)
|
||||
@@ -2,5 +2,12 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit" : "^9",
|
||||
"sempro/phpunit-pretty-print": "^1.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Combodo\\iTop\\Test\\UnitTest\\": "src/BaseTestCase/",
|
||||
"Combodo\\iTop\\Test\\UnitTest\\Hook\\": "src/Hook/",
|
||||
"Combodo\\iTop\\Test\\UnitTest\\Service\\": "src/Service/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
tests/php-unit-tests/experiments/README.md
Normal file
1
tests/php-unit-tests/experiments/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This directory aims at providing experimental proof of the mechanics of PHPUnit
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
|
||||
/**
|
||||
* Shows that
|
||||
* 1) the option runClassInSeparateProcess is equivalent to runTestsInSeparateProcesses
|
||||
* 2) setUpBeforeClass is called within each spawned process (the main one, then in eventuel subprocesses)
|
||||
* 3) setUp behaves as expected, i.e. called one within the same process as the test itself
|
||||
*
|
||||
* @preserveGlobalState disabled
|
||||
* @runClassInSeparateProcess
|
||||
*/
|
||||
class runClassInSeparateProcessTest extends ItopDataTestCase
|
||||
{
|
||||
static public function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass(); // TODO: Change the autogenerated stub
|
||||
|
||||
file_put_contents(
|
||||
dirname(__FILE__).'/pid.txt',
|
||||
getmypid().';'.static::class.';'.__METHOD__."\n",
|
||||
FILE_APPEND);
|
||||
}
|
||||
|
||||
protected function LogPid()
|
||||
{
|
||||
file_put_contents(
|
||||
dirname(__FILE__).'/pid.txt',
|
||||
getmypid().';'.static::class.';'.$this->getName()."\n",
|
||||
FILE_APPEND);
|
||||
}
|
||||
|
||||
function testA()
|
||||
{
|
||||
$this->LogPid();
|
||||
static::assertTrue(true);
|
||||
}
|
||||
|
||||
function testB()
|
||||
{
|
||||
$this->LogPid();
|
||||
static::assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider CProvider
|
||||
*/
|
||||
function testC($i)
|
||||
{
|
||||
$this->LogPid();
|
||||
static::assertTrue(true);
|
||||
}
|
||||
|
||||
function CProvider()
|
||||
{
|
||||
return [
|
||||
[1],
|
||||
[1],
|
||||
[1],
|
||||
[1],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Shows that tearDown is called after a fatal error within a test
|
||||
*/
|
||||
class tearDownAfterFailureTest extends TestCase
|
||||
{
|
||||
static $bIsCorrectlyInitialized = true;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
static::$bIsCorrectlyInitialized = true;
|
||||
}
|
||||
|
||||
function testIsInitializedAndChangeIt()
|
||||
{
|
||||
static::assertTrue(static::$bIsCorrectlyInitialized);
|
||||
|
||||
static::$bIsCorrectlyInitialized = false;
|
||||
|
||||
$this->expectException('Exception');
|
||||
throw new \Exception('hello');
|
||||
}
|
||||
|
||||
function testIsStillInitialized()
|
||||
{
|
||||
static::assertTrue(static::$bIsCorrectlyInitialized);
|
||||
}
|
||||
|
||||
function testFailingDueToUnexpectedException()
|
||||
{
|
||||
static::$bIsCorrectlyInitialized = false;
|
||||
This_Is_Not_A_Function_And_Causes_A_Fatal_Error();
|
||||
}
|
||||
|
||||
function testIsStillInitializedAnyway()
|
||||
{
|
||||
static::assertTrue(static::$bIsCorrectlyInitialized);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2023 Combodo SARL
|
||||
* 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
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use Dict;
|
||||
use const APPROOT;
|
||||
|
||||
/**
|
||||
* As {@see DictionariesConsistencyTest}, we are testing dict files, but the ones that are compiled, so we cannot be in the beforeSetup group !
|
||||
*/
|
||||
class CompiledDictionariesConsistencyTest extends ItopTestCase
|
||||
{
|
||||
/**
|
||||
* make sure N°5305 dictionary changes (CSV import ergonomy) are still here and UI remains unbroken for any lang
|
||||
*
|
||||
* One of the things checked is the number of parameters in the dict value. This is for now crashing the app (N°5491)
|
||||
* and we have multiple inconsistencies in our existing dict files... So it is complicated to have a generic test for all files !
|
||||
* At least we are protecting those new entries...
|
||||
*/
|
||||
public function testImportCsvMessageStillOk()
|
||||
{
|
||||
$aFailedLabels = [];
|
||||
$aLabelsToTest = [
|
||||
'UI:CSVReport-Value-SetIssue' => [],
|
||||
'UI:CSVReport-Value-ChangeIssue' => ['arg1'],
|
||||
'UI:CSVReport-Value-NoMatch' => ['arg1'],
|
||||
'UI:CSVReport-Value-NoMatch-PossibleValues' => ['arg1', 'arg2'],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject' => ['arg1'],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => ['arg1'],
|
||||
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => ['arg1'],
|
||||
];
|
||||
|
||||
$sCompiledLanguagesFilePath = APPROOT . 'env-' . \utils::GetCurrentEnvironment() . '/dictionaries/languages.php';
|
||||
$this->assertFileExists($sCompiledLanguagesFilePath, 'We must have an existing compiled language.php file in the current env !');
|
||||
require_once($sCompiledLanguagesFilePath);
|
||||
$this->assertNotEmpty(Dict::GetLanguages(), 'the languages.php file exists but didn\'t load any language');
|
||||
|
||||
foreach (glob(APPROOT . 'env-' . \utils::GetCurrentEnvironment() . '/dictionaries/*.dict.php') as $sDictFile) {
|
||||
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)) {
|
||||
$sLangCode = $aMatches[1];
|
||||
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
|
||||
Dict::SetUserLanguage($sLanguageCode);
|
||||
|
||||
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs) {
|
||||
echo "Testing $sDictFile, label $sLabelKey with " . \var_export($aLabelArgs, true) . "\n";
|
||||
try {
|
||||
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
|
||||
//$this->debug($sLabelValue);
|
||||
} catch (\ValueError $e) {
|
||||
$aFailedLabels[] = $sLabelKey;
|
||||
|
||||
$this->debug([
|
||||
'exception' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'label_name' => $sLabelKey,
|
||||
'label_args' => $aLabelArgs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->assertEquals([], $aFailedLabels, "$sDictFile : test fail for lang $sLangCode and labels (" . implode(", ", $aFailedLabels) . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use GlobIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveRegexIterator;
|
||||
use RegexIterator;
|
||||
|
||||
/**
|
||||
* Performs code static analysis to detect patterns that will change the values of static data and therefor could affect other tests while running them in a single process
|
||||
*
|
||||
* @runClassInSeparateProcess
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
class detectStaticPollutionTest extends TestCase
|
||||
{
|
||||
protected function FindMatches($sFile, $sFileContents, $sRegexp)
|
||||
{
|
||||
$aRes = [];
|
||||
foreach (explode("\n", $sFileContents) as $iLine => $sLine) {
|
||||
if (preg_match_all($sRegexp, $sLine, $aMatches, PREG_PATTERN_ORDER)) {
|
||||
$sLine = $iLine + 1;
|
||||
$aRes[] = "$sFile:$sLine";
|
||||
}
|
||||
}
|
||||
return $aRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider PollutingPatterns
|
||||
* @param $sPattern
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function testDetectPolluters($sPattern, $sFix)
|
||||
{
|
||||
$sScannedDir = dirname(__FILE__).'/../unitary-tests';
|
||||
|
||||
$aPolluters = [];
|
||||
$oDirectory = new RecursiveDirectoryIterator($sScannedDir);
|
||||
$Iterator = new RecursiveIteratorIterator($oDirectory);
|
||||
foreach (new RegexIterator($Iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH) as $aMatch) {
|
||||
$sFile = $aMatch[0];
|
||||
if(is_file($sFile)) {
|
||||
$sFileContents = file_get_contents($sFile);
|
||||
if (preg_match_all($sPattern, $sFileContents, $keys, PREG_PATTERN_ORDER)) {
|
||||
$aPolluters = array_merge($aPolluters, $this->FindMatches($sFile, $sFileContents, $sPattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
$iPolluters = count($aPolluters);
|
||||
static::assertTrue($iPolluters === 0, "Found polluter(s) for pattern $sPattern, $sFix:\n".implode("\n", $aPolluters));
|
||||
|
||||
}
|
||||
|
||||
public function PollutingPatterns()
|
||||
{
|
||||
return [
|
||||
'ContextTags' => ['/ContextTag::AddContext/i', 'Use new ContextTag() instead'],
|
||||
'Dict::Add' => ['/Dict::Add/i', 'TODO: implement a facade into ItopDataTestCase'],
|
||||
'EventService::RegisterListener' => ['/EventService::RegisterListener/i', 'Use ItopDataTestCase::EventService_RegisterListener instead'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,9 +16,9 @@
|
||||
namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use Dict;
|
||||
|
||||
/**
|
||||
* For tests on compiled dict files, see {@see CompiledDictionariesConsistencyTest}
|
||||
* @group beforeSetup
|
||||
*/
|
||||
class DictionariesConsistencyTest extends ItopTestCase
|
||||
@@ -32,6 +32,8 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
*/
|
||||
public function testDictionariesLanguage($sDictFile): void
|
||||
{
|
||||
// In iTop the language available list is dynamically made during setup, depending on the dict files found
|
||||
// Here we are using a fixed list
|
||||
$aPrefixToLanguageData = array(
|
||||
'cs' => array('CS CZ', 'Czech', 'Čeština'),
|
||||
'da' => array('DA DA', 'Danish', 'Dansk'),
|
||||
@@ -96,10 +98,12 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
{
|
||||
$this->setUp();
|
||||
|
||||
$sAppRoot = $this->GetAppRoot();
|
||||
|
||||
$aDictFiles = array_merge(
|
||||
glob(APPROOT.'datamodels/2.x/*/*.dict*.php'), // legacy form in modules
|
||||
glob(APPROOT.'datamodels/2.x/*/dictionaries/*.dict*.php'), // modern form in modules
|
||||
glob(APPROOT.'dictionaries/*.dict*.php') // framework
|
||||
glob($sAppRoot.'datamodels/2.x/*/*.dict*.php'), // legacy form in modules
|
||||
glob($sAppRoot.'datamodels/2.x/*/dictionaries/*.dict*.php'), // modern form in modules
|
||||
glob($sAppRoot.'dictionaries/*.dict*.php') // framework
|
||||
);
|
||||
$aTestCases = array();
|
||||
foreach ($aDictFiles as $sDictFile) {
|
||||
@@ -150,67 +154,4 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
|
||||
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ImportCsvMessageStillOkProvider
|
||||
* make sure N°5305 dictionary changes are still here and UI remains unbroken for any lang
|
||||
*/
|
||||
public function testImportCsvMessageStillOk($sLangCode, $sDictFile)
|
||||
{
|
||||
$aFailedLabels = [];
|
||||
$aLabelsToTest = [
|
||||
'UI:CSVReport-Value-SetIssue' => [],
|
||||
'UI:CSVReport-Value-ChangeIssue' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-PossibleValues' => [ 'arg1', 'arg2' ],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => [ 'arg1' ],
|
||||
];
|
||||
|
||||
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
|
||||
require_once(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/languages.php');
|
||||
Dict::SetUserLanguage($sLanguageCode);
|
||||
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs){
|
||||
try{
|
||||
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
|
||||
//$this->debug($sLabelValue);
|
||||
} catch (\ValueError $e){
|
||||
$aFailedLabels[] = $sLabelKey;
|
||||
|
||||
$this->debug([
|
||||
'exception' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'label_name' => $sLabelKey,
|
||||
'label_args' =>$aLabelArgs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->assertEquals([], $aFailedLabels, "test fail for lang $sLangCode and labels (" . implode(", ", $aFailedLabels) . ')');
|
||||
}
|
||||
|
||||
public function ImportCsvMessageStillOkProvider(){
|
||||
return $this->GetDictFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* return a map linked to *.dict.php files that are generated after setup
|
||||
* each entry key is lang code (example 'en')
|
||||
* each value is an array with lang code (again) and dict file path
|
||||
* @return array
|
||||
*/
|
||||
private function GetDictFiles() : array {
|
||||
$aDictFiles = [];
|
||||
|
||||
foreach (glob(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/*.dict.php') as $sDictFile){
|
||||
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)){
|
||||
$sLangCode = $aMatches[1];
|
||||
$aDictFiles[$sLangCode] = [
|
||||
'lang' => $sLangCode,
|
||||
'file' => $sDictFile
|
||||
];
|
||||
}
|
||||
}
|
||||
return $aDictFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use utils;
|
||||
|
||||
/**
|
||||
* @package Combodo\iTop\Test\UnitTest\Setup
|
||||
* @group beforeSetup
|
||||
*/
|
||||
class iTopModulesPhpVersionIntegrationTest extends ItopTestCase {
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,7 @@ use iTopDesignFormat;
|
||||
* @covers iTopDesignFormat
|
||||
*
|
||||
* @package Combodo\iTop\Test\UnitTest\Setup
|
||||
* @group beforeSetup
|
||||
*/
|
||||
class iTopModulesXmlVersionIntegrationTest extends ItopTestCase
|
||||
{
|
||||
@@ -71,11 +72,13 @@ class iTopModulesXmlVersionIntegrationTest extends ItopTestCase
|
||||
{
|
||||
static::setUp();
|
||||
|
||||
$sPath = APPROOT.'datamodels/2.x/*/datamodel.*.xml';
|
||||
$sAppRoot = $this->GetAppRoot();
|
||||
|
||||
$sPath = $sAppRoot.'datamodels/2.x/*/datamodel.*.xml';
|
||||
$aXmlFiles = glob($sPath);
|
||||
|
||||
$aXmlFiles[] = APPROOT.'core/datamodel.core.xml';
|
||||
$aXmlFiles[] = APPROOT.'application/datamodel.application.xml';
|
||||
$aXmlFiles[] = $sAppRoot.'core/datamodel.core.xml';
|
||||
$aXmlFiles[] = $sAppRoot.'application/datamodel.application.xml';
|
||||
|
||||
$aTestCases = array();
|
||||
foreach ($aXmlFiles as $sXmlFile) {
|
||||
|
||||
@@ -20,6 +20,7 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
|
||||
/**
|
||||
* @package Combodo\iTop\Test\UnitTest\Setup
|
||||
* @group beforeSetup
|
||||
*/
|
||||
class iTopXmlVersionIntegrationTest extends ItopTestCase
|
||||
{
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9"
|
||||
>
|
||||
|
||||
<extensions>
|
||||
<extension class="Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook" />
|
||||
</extensions>
|
||||
|
||||
<php>
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
<ini name="error_reporting" value="E_ALL"/>
|
||||
<ini name="display_errors" value="On"/>
|
||||
<ini name="log_errors" value="On"/>
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9"
|
||||
>
|
||||
|
||||
<extensions>
|
||||
<extension class="Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook" />
|
||||
</extensions>
|
||||
|
||||
<php>
|
||||
<ini name="error_reporting" value="E_ALL"/>
|
||||
<ini name="display_errors" value="On"/>
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook;
|
||||
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
|
||||
use Config;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
|
||||
/**
|
||||
* Class ItopCustomDatamodelTestCase
|
||||
*
|
||||
* Helper class to extend for tests needing a custom DataModel (eg. classes, attributes, etc conditions not available in the standard DM)
|
||||
* Usage:
|
||||
* - Create a test case class extending this one
|
||||
* - Override the {@see ItopCustomDatamodelTestCase::GetDatamodelDeltaAbsPath()} method to define where you XML delta is
|
||||
* - Implement your test case methods as usual
|
||||
*
|
||||
* @since N°6097 2.7.9 3.0.4 3.1.0
|
||||
*/
|
||||
abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
|
||||
{
|
||||
/**
|
||||
* @var bool[]
|
||||
*/
|
||||
protected static $aReadyCustomEnvironments = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @since N°6097 Workaround to make the "runClassInSeparateProcess" directive work
|
||||
*/
|
||||
public function __construct($name = null, array $data = [], $dataName = '')
|
||||
{
|
||||
parent::__construct($name, $data, $dataName);
|
||||
|
||||
// Ensure that a test class derived from this one runs in a dedicated process as it changes the MetaModel / environment on the fly and
|
||||
// for now we have no way of switching environments properly in memory and it will result in other (regular) test classes to fail as they won't be on the expected environment.
|
||||
//
|
||||
// If we don't do this, we would have to add the `@runTestsInSeparateProcesses` on *each* test classes which we want to avoid for obvious possible mistakes.
|
||||
// Note that the `@runClassInSeparateProcess` don't work in PHPUnit yet.
|
||||
$this->setRunClassInSeparateProcess(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Abs path to the XML delta to use for the tests of that class
|
||||
*/
|
||||
abstract public function GetDatamodelDeltaAbsPath(): string;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function LoadRequiredItopFiles(): void
|
||||
{
|
||||
parent::LoadRequiredItopFiles();
|
||||
|
||||
$this->RequireOnceItopFile('setup/setuputils.class.inc.php');
|
||||
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Environment used as a base (conf. file, modules, DB, ...) to prepare the test environment
|
||||
*/
|
||||
protected function GetSourceEnvironment(): string
|
||||
{
|
||||
return 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @warning This should ONLY be overloaded if your test case XML deltas are NOT compatible with the others, as it will create / compile another environment, increasing the global testing time.
|
||||
*/
|
||||
public function GetTestEnvironment(): string
|
||||
{
|
||||
return 'php-unit-tests';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Absolute path to the {@see \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase::GetTestEnvironment()} folder
|
||||
*/
|
||||
final private function GetTestEnvironmentFolderAbsPath(): string
|
||||
{
|
||||
return APPROOT.'env-'.$this->GetTestEnvironment().'/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} as ready (compiled)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final private function MarkEnvironmentReady(): void
|
||||
{
|
||||
if (false === $this->IsEnvironmentReady()) {
|
||||
touch(static::GetTestEnvironmentFolderAbsPath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if the {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} is ready (compiled, but not started)
|
||||
*
|
||||
* @details Having the environment ready means that it has been compiled for this global tests run, not that it is a relic from a previous global tests run
|
||||
*/
|
||||
final private function IsEnvironmentReady(): bool
|
||||
{
|
||||
// As these test cases run in separate processes, the best way we found to let know a process if its environment was already prepared for **this run** was to compare the modification times of:
|
||||
// - its own env-<ENV> folder
|
||||
// - a file generated at the beginning of the global test run {@see \Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook}
|
||||
$sRunStartedFilePath = TestsRunStartHook::GetRunStartedFileAbsPath();
|
||||
$sEnvFolderPath = static::GetTestEnvironmentFolderAbsPath();
|
||||
|
||||
clearstatcache();
|
||||
if (false === file_exists($sRunStartedFilePath) || false === file_exists($sEnvFolderPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$iRunStartedFileModificationTime = filemtime($sRunStartedFilePath);
|
||||
$iEnvFolderModificationTime = filemtime($sEnvFolderPath);
|
||||
|
||||
return $iEnvFolderModificationTime >= $iRunStartedFileModificationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function PrepareEnvironment(): void
|
||||
{
|
||||
$sSourceEnv = $this->GetSourceEnvironment();
|
||||
$sTestEnv = $this->GetTestEnvironment();
|
||||
|
||||
// Check if test env. is already set and only prepare it if it's not up-to-date
|
||||
//
|
||||
// Note: To improve performances, we compile all XML deltas from test cases derived from this class and make a single environment where everything will be ran at once.
|
||||
// This requires XML deltas to be compatible, but it is a known and accepted trade-off. See PR #457
|
||||
if (false === $this->IsEnvironmentReady()) {
|
||||
//----------------------------------------------------
|
||||
// Clear any previous "$sTestEnv" environment
|
||||
//----------------------------------------------------
|
||||
|
||||
// - Configuration file
|
||||
$sConfFile = utils::GetConfigFilePath($sTestEnv);
|
||||
$sConfFolder = dirname($sConfFile);
|
||||
if (is_file($sConfFile)) {
|
||||
chmod($sConfFile, 0777);
|
||||
SetupUtils::tidydir($sConfFolder);
|
||||
}
|
||||
|
||||
// - Datamodel delta files
|
||||
// - Cache folder
|
||||
// - Compiled folder
|
||||
// We don't need to clean them as they are already by the compilation
|
||||
|
||||
// - Drop database
|
||||
// We don't do that now, it will be done before re-creating the DB, once the metamodel is started
|
||||
|
||||
//----------------------------------------------------
|
||||
// Prepare "$sTestEnv" environment
|
||||
//----------------------------------------------------
|
||||
|
||||
// All the following is greatly inspired by the toolkit's sandbox script
|
||||
// - Prepare config file
|
||||
$oSourceConf = new Config(utils::GetConfigFilePath($sSourceEnv));
|
||||
if ($oSourceConf->Get('source_dir') === '') {
|
||||
throw new Exception('Missing entry source_dir from the config file');
|
||||
}
|
||||
|
||||
$oTestConfig = clone($oSourceConf);
|
||||
$oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv);
|
||||
// - Switch DB name to a dedicated one so we don't mess with the original one
|
||||
$sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv);
|
||||
$oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName);
|
||||
|
||||
// - Compile env. based on the existing 'production' env.
|
||||
$oEnvironment = new UnitTestRunTimeEnvironment($sTestEnv);
|
||||
$oEnvironment->WriteConfigFileSafe($oTestConfig);
|
||||
$oEnvironment->CompileFrom($sSourceEnv, false);
|
||||
|
||||
// - Force re-creating a fresh DB
|
||||
CMDBSource::InitFromConfig($oTestConfig);
|
||||
if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) {
|
||||
CMDBSource::DropDB();
|
||||
}
|
||||
CMDBSource::CreateDB($oTestConfig->Get('db_name'));
|
||||
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv);
|
||||
|
||||
$this->MarkEnvironmentReady();
|
||||
$this->debug('Preparation of custom environment "'.$sTestEnv.'" done.');
|
||||
}
|
||||
|
||||
parent::PrepareEnvironment();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
<?php
|
||||
// Copyright (c) 2010-2023 Combodo SARL
|
||||
//
|
||||
// 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
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
//
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
|
||||
@@ -31,6 +18,7 @@ use CMDBObject;
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Config;
|
||||
use Contact;
|
||||
use DBObject;
|
||||
use DBObjectSet;
|
||||
@@ -46,11 +34,13 @@ use MetaModel;
|
||||
use Person;
|
||||
use PluginManager;
|
||||
use Server;
|
||||
use SetupUtils;
|
||||
use TagSetFieldData;
|
||||
use Ticket;
|
||||
use URP_UserProfile;
|
||||
use User;
|
||||
use UserRequest;
|
||||
use utils;
|
||||
use VirtualHost;
|
||||
use VirtualMachine;
|
||||
use XMLDataLoader;
|
||||
@@ -61,41 +51,53 @@ define('TAG_CLASS', 'FAQ');
|
||||
define('TAG_ATTCODE', 'domains');
|
||||
|
||||
/**
|
||||
* Class ItopDataTestCase
|
||||
*
|
||||
* Helper class to extend for tests needing access to iTop's metamodel
|
||||
*
|
||||
* **⚠ Warning** Each class extending this one needs to add the following annotations :
|
||||
*
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*
|
||||
* @since 2.7.7 3.0.1 3.1.0 N°4624 processIsolation is disabled by default and must be enabled in each test needing it (basically all tests using
|
||||
* iTop datamodel)
|
||||
*/
|
||||
class ItopDataTestCase extends ItopTestCase
|
||||
abstract class ItopDataTestCase extends ItopTestCase
|
||||
{
|
||||
private $iTestOrgId;
|
||||
|
||||
// For cleanup
|
||||
private $aCreatedObjects = array();
|
||||
|
||||
// Counts
|
||||
public $aReloadCount = [];
|
||||
private $aCreatedObjects = [];
|
||||
private $aEventListeners = [];
|
||||
|
||||
/**
|
||||
* @var string Default environment to use for test cases
|
||||
*/
|
||||
const DEFAULT_TEST_ENVIRONMENT = 'production';
|
||||
const USE_TRANSACTION = true;
|
||||
const CREATE_TEST_ORG = false;
|
||||
|
||||
/**
|
||||
* This method is called before the first test of this test class is run (in the current process).
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after the last test of this test class is run (in the current process).
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
\UserRights::FlushPrivileges();
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->RequireOnceItopFile('application/utils.inc.php');
|
||||
|
||||
$sEnv = 'production';
|
||||
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
$this->PrepareEnvironment();
|
||||
|
||||
if (static::USE_TRANSACTION)
|
||||
{
|
||||
@@ -105,8 +107,6 @@ class ItopDataTestCase extends ItopTestCase
|
||||
{
|
||||
$this->CreateTestOrganization();
|
||||
}
|
||||
|
||||
EventService::RegisterListener(EVENT_DB_OBJECT_RELOAD, [$this, 'CountObjectReload']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +114,9 @@ class ItopDataTestCase extends ItopTestCase
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
static::SetNonPublicStaticProperty(\cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged', []);
|
||||
\cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
|
||||
|
||||
if (static::USE_TRANSACTION) {
|
||||
$this->debug("ROLLBACK !!!");
|
||||
CMDBSource::Query('ROLLBACK');
|
||||
@@ -135,10 +138,65 @@ class ItopDataTestCase extends ItopTestCase
|
||||
}
|
||||
}
|
||||
}
|
||||
// As soon as a rollback has been performed, each object memoized should be discarded
|
||||
CMDBObject::SetCurrentChange(null);
|
||||
|
||||
// Leave the place clean
|
||||
\UserRights::Logoff();
|
||||
|
||||
foreach ($this->aEventListeners as $sListenerId) {
|
||||
EventService::UnRegisterListener($sListenerId);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function LoadRequiredItopFiles(): void
|
||||
{
|
||||
parent::LoadRequiredItopFiles();
|
||||
|
||||
$this->RequireOnceItopFile('application/utils.inc.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Environment the test will run in
|
||||
* @since 2.7.9 3.0.4 3.1.0
|
||||
*/
|
||||
protected function GetTestEnvironment(): string
|
||||
{
|
||||
return self::DEFAULT_TEST_ENVIRONMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Absolute path of the configuration file used for the test
|
||||
* @since 2.7.9 3.0.4 3.1.0
|
||||
*/
|
||||
protected function GetConfigFileAbsPath(): string
|
||||
{
|
||||
return utils::GetConfigFilePath($this->GetTestEnvironment());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the iTop environment for test to run
|
||||
*
|
||||
* @return void
|
||||
* @throws \CoreException
|
||||
* @throws \DictExceptionUnknownLanguage
|
||||
* @throws \MySQLException
|
||||
* @since 2.7.9 3.0.4 3.1.0
|
||||
*/
|
||||
protected function PrepareEnvironment(): void
|
||||
{
|
||||
$sEnv = $this->GetTestEnvironment();
|
||||
$sConfigFile = $this->GetConfigFileAbsPath();
|
||||
|
||||
// Start MetaModel for the prepared environment
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -147,6 +205,31 @@ class ItopDataTestCase extends ItopTestCase
|
||||
return $this->iTestOrgId;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
/// Facades for environment settings
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* Facade for EventService::RegisterListener
|
||||
*
|
||||
* @param string $sEvent
|
||||
* @param callable $callback
|
||||
* @param $sEventSource
|
||||
* @param array $aCallbackData
|
||||
* @param $context
|
||||
* @param float $fPriority
|
||||
* @param $sModuleId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function EventService_RegisterListener(string $sEvent, callable $callback, $sEventSource = null, array $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string
|
||||
{
|
||||
$ret = EventService::RegisterListener($sEvent, $callback, $sEventSource, $aCallbackData, $context, $fPriority, $sModuleId);
|
||||
if (false !== $ret) {
|
||||
$this->aEventListeners[] = $ret;
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
/// MetaModel Utilities
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
@@ -841,49 +924,6 @@ class ItopDataTestCase extends ItopTestCase
|
||||
return $oOrg;
|
||||
}
|
||||
|
||||
public function ResetReloadCount()
|
||||
{
|
||||
$this->aReloadCount = [];
|
||||
}
|
||||
|
||||
public function DebugReloadCount($sMsg, $bResetCount = true)
|
||||
{
|
||||
$iTotalCount = 0;
|
||||
$aTotalPerClass = [];
|
||||
foreach ($this->aReloadCount as $sClass => $aCountByKeys) {
|
||||
$iClassCount = 0;
|
||||
foreach ($aCountByKeys as $iCount) {
|
||||
$iClassCount += $iCount;
|
||||
}
|
||||
$iTotalCount += $iClassCount;
|
||||
$aTotalPerClass[$sClass] = $iClassCount;
|
||||
}
|
||||
$this->debug("$sMsg - $iTotalCount reload(s)");
|
||||
foreach ($this->aReloadCount as $sClass => $aCountByKeys) {
|
||||
$this->debug(" $sClass => $aTotalPerClass[$sClass] reload(s)");
|
||||
foreach ($aCountByKeys as $sKey => $iCount) {
|
||||
$this->debug(" $sClass::$sKey => $iCount");
|
||||
}
|
||||
}
|
||||
if ($bResetCount) {
|
||||
$this->ResetReloadCount();
|
||||
}
|
||||
}
|
||||
|
||||
public function CountObjectReload(EventData $oData)
|
||||
{
|
||||
$oObject = $oData->Get('object');
|
||||
$sClass = get_class($oObject);
|
||||
$sKey = $oObject->GetKey();
|
||||
$iCount = $this->GetObjectReloadCount($sClass, $sKey);
|
||||
$this->aReloadCount[$sClass][$sKey] = 1 + $iCount;
|
||||
}
|
||||
|
||||
public function GetObjectReloadCount($sClass, $sKey)
|
||||
{
|
||||
return $this->aReloadCount[$sClass][$sKey] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a series of operations will trigger a given number of MySL queries
|
||||
*
|
||||
@@ -910,6 +950,17 @@ class ItopDataTestCase extends ItopTestCase
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertDBChangeOpCount(string $sClass, $iId, int $iExpectedCount)
|
||||
{
|
||||
$oSearch = new \DBObjectSearch('CMDBChangeOp');
|
||||
$oSearch->AddCondition('objclass', $sClass);
|
||||
$oSearch->AddCondition('objkey', $iId);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
$iCount = $oSet->Count();
|
||||
$this->assertEquals($iExpectedCount, $iCount, "Found $iCount changes for object $sClass::$iId");
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a set of XML files describing a consistent set of iTop objects
|
||||
* @param string[] $aFiles
|
||||
@@ -1,61 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2023 Combodo SARL
|
||||
*
|
||||
* 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
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest;
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: Eric
|
||||
* Date: 20/11/2017
|
||||
* Time: 11:21
|
||||
*/
|
||||
|
||||
use CMDBSource;
|
||||
use MySQLTransactionNotClosedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SetupUtils;
|
||||
|
||||
class ItopTestCase extends TestCase
|
||||
/**
|
||||
* Class ItopTestCase
|
||||
*
|
||||
* Helper class to extend for tests that DO NOT need to access the DataModel or the Database
|
||||
*
|
||||
* @author Eric Espie <eric.espie@combodo.com>
|
||||
* @package Combodo\iTop\Test\UnitTest
|
||||
*/
|
||||
abstract class ItopTestCase extends TestCase
|
||||
{
|
||||
const TEST_LOG_DIR = 'test';
|
||||
static $DEBUG_UNIT_TEST = false;
|
||||
public const TEST_LOG_DIR = 'test';
|
||||
public static $DEBUG_UNIT_TEST = false;
|
||||
|
||||
/** @noinspection UsingInclusionOnceReturnValueInspection avoid errors for approot includes */
|
||||
protected function setUp(): void {
|
||||
$sAppRootRelPath = 'approot.inc.php';
|
||||
$sDepthSeparator = '../';
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
if (file_exists($sAppRootRelPath)) {
|
||||
require_once $sAppRootRelPath;
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Override the default value to disable the backup of globals in case of tests run in a separate process
|
||||
*/
|
||||
protected $preserveGlobalState = false;
|
||||
|
||||
$sAppRootRelPath = $sDepthSeparator.$sAppRootRelPath;
|
||||
}
|
||||
/**
|
||||
* This method is called before the first test of this test class is run (in the current process).
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
static::$DEBUG_UNIT_TEST = getenv('DEBUG_UNIT_TEST');
|
||||
|
||||
require_once static::GetAppRoot() . 'approot.inc.php';
|
||||
|
||||
if (false === defined('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME')) {
|
||||
// setUp might be called multiple times, so protecting the define() call !
|
||||
define('ITOP_PHPUNIT_RUNNING_CONSTANT_NAME', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after the last test of this test class is run (in the current process).
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
|
||||
if (method_exists('utils', 'GetConfig')) {
|
||||
// Reset the config by forcing the load from disk
|
||||
$oConfig = \utils::GetConfig(true);
|
||||
if (method_exists('MetaModel', 'SetConfig')) {
|
||||
\MetaModel::SetConfig($oConfig);
|
||||
}
|
||||
}
|
||||
if (method_exists('Dict', 'SetUserLanguage')) {
|
||||
\Dict::SetUserLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->debug("\n----------\n---------- ".$this->getName()."\n----------\n");
|
||||
|
||||
if (false === defined(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME)) {
|
||||
// setUp might be called multiple times, so protecting the define() call !
|
||||
define(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME, true);
|
||||
}
|
||||
$this->LoadRequiredItopFiles();
|
||||
$this->LoadRequiredTestFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,10 +84,60 @@ class ItopTestCase extends TestCase
|
||||
|
||||
if (CMDBSource::IsInsideTransaction()) {
|
||||
// Nested transactions were opened but not finished !
|
||||
// Rollback to avoid side effects on next tests
|
||||
while (CMDBSource::IsInsideTransaction()) {
|
||||
CMDBSource::Query('ROLLBACK');
|
||||
}
|
||||
throw new MySQLTransactionNotClosedException('Some DB transactions were opened but not closed ! Fix the code by adding ROLLBACK or COMMIT statements !', []);
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper than can be called in the context of a data provider */
|
||||
public static function GetAppRoot()
|
||||
{
|
||||
if (defined('APPROOT')) {
|
||||
return APPROOT;
|
||||
}
|
||||
$sSearchPath = __DIR__;
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
if (file_exists($sSearchPath.'/approot.inc.php')) {
|
||||
break;
|
||||
}
|
||||
$iOffsetSep = strrpos($sSearchPath, '/');
|
||||
if ($iOffsetSep === false) {
|
||||
$iOffsetSep = strrpos($sSearchPath, '\\');
|
||||
if ($iOffsetSep === false) {
|
||||
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
|
||||
return 'Could not find the approot file in '.$sSearchPath;
|
||||
}
|
||||
}
|
||||
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
|
||||
}
|
||||
return $sSearchPath.'/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceItopFile()}
|
||||
*
|
||||
* @return void
|
||||
* @since 2.7.9 3.0.4 3.1.0
|
||||
*/
|
||||
protected function LoadRequiredItopFiles(): void
|
||||
{
|
||||
// Empty until we actually need to require some files in the class
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}
|
||||
*
|
||||
* @return void
|
||||
* @since 2.7.10 3.0.4 3.1.0
|
||||
*/
|
||||
protected function LoadRequiredTestFiles(): void
|
||||
{
|
||||
// Empty until we actually need to require some files in the class
|
||||
}
|
||||
|
||||
/**
|
||||
* Require once an iTop file (core or extension) from its relative path to the iTop root dir.
|
||||
* This ensure to always use the right absolute path, especially in {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}
|
||||
@@ -83,7 +149,7 @@ class ItopTestCase extends TestCase
|
||||
*/
|
||||
protected function RequireOnceItopFile(string $sFileRelPath): void
|
||||
{
|
||||
require_once APPROOT . $sFileRelPath;
|
||||
require_once $this->GetAppRoot() . $sFileRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +217,7 @@ class ItopTestCase extends TestCase
|
||||
/**
|
||||
* @since 2.7.4 3.0.0
|
||||
*/
|
||||
public function InvokeNonPublicStaticMethod($sObjectClass, $sMethodName, $aArgs)
|
||||
public function InvokeNonPublicStaticMethod($sObjectClass, $sMethodName, $aArgs = [])
|
||||
{
|
||||
return $this->InvokeNonPublicMethod($sObjectClass, $sMethodName, null, $aArgs);
|
||||
}
|
||||
@@ -168,7 +234,7 @@ class ItopTestCase extends TestCase
|
||||
*
|
||||
* @since 2.7.4 3.0.0
|
||||
*/
|
||||
public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs)
|
||||
public function InvokeNonPublicMethod($sObjectClass, $sMethodName, $oObject, $aArgs = [])
|
||||
{
|
||||
$class = new \ReflectionClass($sObjectClass);
|
||||
$method = $class->getMethod($sMethodName);
|
||||
63
tests/php-unit-tests/src/Hook/TestsRunStartHook.php
Normal file
63
tests/php-unit-tests/src/Hook/TestsRunStartHook.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Hook;
|
||||
|
||||
require_once __DIR__ . '/../../../../approot.inc.php';
|
||||
|
||||
use PHPUnit\Runner\AfterLastTestHook;
|
||||
use PHPUnit\Runner\BeforeFirstTestHook;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class TestsRunStartHook
|
||||
*
|
||||
* IMPORTANT: This will no longer work in PHPUnit 10.0 and there is no alternative for now, so we will have to migrate it when the time comes
|
||||
* @link https://localheinz.com/articles/2023/02/14/extending-phpunit-with-its-new-event-system/#content-hooks-event-system
|
||||
*
|
||||
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
|
||||
* @package Combodo\iTop\Test\UnitTest\Hook
|
||||
* @since N°6097 2.7.10 3.0.4 3.1.1
|
||||
*/
|
||||
class TestsRunStartHook implements BeforeFirstTestHook, AfterLastTestHook
|
||||
{
|
||||
/**
|
||||
* Use the modification time on this file to check whereas it is newer than the requirements in a test case
|
||||
*
|
||||
* @return string Abs. path to a file generated when the global tests run starts.
|
||||
*/
|
||||
public static function GetRunStartedFileAbsPath(): string
|
||||
{
|
||||
// Note: This can't be put in the cache-<ENV> folder as we have multiple <ENV> running across the test cases
|
||||
// We also don't want to put it in the unit tests folder as it is not supposed to be writable
|
||||
return APPROOT.'data/.php-unit-tests-run-started';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function executeBeforeFirstTest(): void
|
||||
{
|
||||
// Create / change modification timestamp of file marking the beginning of the tests run
|
||||
touch(static::GetRunStartedFileAbsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function executeAfterLastTest(): void
|
||||
{
|
||||
// Cleanup of file marking the beginning of the tests run
|
||||
if (file_exists(static::GetRunStartedFileAbsPath())) {
|
||||
unlink(static::GetRunStartedFileAbsPath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Service;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
|
||||
use IssueLog;
|
||||
use MFCoreModule;
|
||||
use ReflectionClass;
|
||||
use RunTimeEnvironment;
|
||||
|
||||
|
||||
/**
|
||||
* Class UnitTestRunTimeEnvironment
|
||||
*
|
||||
* Runtime env. dedicated to creating a temp. environment for a group of unit tests with XML deltas.
|
||||
*
|
||||
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
|
||||
* @since N°6097 2.7.10 3.0.4 3.1.1
|
||||
*/
|
||||
class UnitTestRunTimeEnvironment extends RunTimeEnvironment
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
|
||||
{
|
||||
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);
|
||||
|
||||
/** @var string[] $aDeltaFiles Referential of loaded deltas. Mostly to avoid duplicates. */
|
||||
$aDeltaFiles = [];
|
||||
foreach (get_declared_classes() as $sClass) {
|
||||
// Filter on classes derived from this \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCaseItopCustomDatamodelTestCase
|
||||
if (false === is_a($sClass, ItopCustomDatamodelTestCase::class, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oReflectionClass = new ReflectionClass($sClass);
|
||||
$oReflectionMethod = $oReflectionClass->getMethod('GetDatamodelDeltaAbsPath');
|
||||
|
||||
// Filter on classes with an actual XML delta (eg. not \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase and maybe some other deriving from a class with a delta)
|
||||
if ($oReflectionMethod->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase $oTestClassInstance */
|
||||
$oTestClassInstance = new $sClass();
|
||||
|
||||
// Check test class is for desired environment
|
||||
if ($oTestClassInstance->GetTestEnvironment() !== $this->sFinalEnv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check XML delta actually exists
|
||||
$sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath();
|
||||
if (false === is_file($sDeltaFile)) {
|
||||
$this->fail("Could not prepare '$this->sFinalEnv' as the XML delta file '$sDeltaFile' (used in $sClass) does not seem to exist");
|
||||
}
|
||||
|
||||
// Avoid duplicates
|
||||
if (in_array($sDeltaFile, $aDeltaFiles)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare fake module name for delta
|
||||
$sDeltaName = preg_replace('/[^\d\w]/', '', $sDeltaFile);
|
||||
// Note: We can't use \MFDeltaModule as we can't specify the ID which leads to only 1 delta being applied... In the future we might introduce a new MFXXXModule, but in the meantime it feels alright (GLA / RQU)
|
||||
$oDelta = new MFCoreModule($sDeltaName, $sDeltaName, $sDeltaFile);
|
||||
|
||||
IssueLog::Debug('XML delta found for unit tests', static::class, [
|
||||
'Unit test class' => $sClass,
|
||||
'Delta file path' => $sDeltaFile,
|
||||
]);
|
||||
|
||||
$aDeltaFiles[] = $sDeltaFile;
|
||||
$aRet[$sDeltaName] = $oDelta;
|
||||
}
|
||||
|
||||
return $aRet;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user