Compare commits

...

59 Commits

Author SHA1 Message Date
denis.flaven@combodo.com
9d6f4569ef Adapt Welcome Popup API for the 3.0.x branch 2023-05-30 08:00:32 +02:00
denis.flaven@combodo.com
1e41e805a2 API for Welcome Popup 2023-05-25 18:04:40 +02:00
odain
bd1e4389f7 N°6293 - [ERGO] Symplify avatar menu - first prototype to sort user menus after setup on page loading 2023-05-22 16:44:57 +02:00
odain
b059fb72a2 Merge branch 'support/3.0' into saas/3.0 2023-05-22 14:08:02 +02:00
odain
1b3b2e8a69 N°6171 - Password Expiration: can expire mode has no effect on user who have never changed their password 2023-05-05 11:44:26 +02:00
jf-cbd
6b448e29f5 PR fix 2023-04-19 10:46:36 +02:00
jf-cbd
7cb6af0a2b fix for 6179 (with description instead of tooltip)
PR fix
2023-04-19 10:46:36 +02:00
jf-cbd
ddc9952ec1 N°6179 - Tooltip attribute in field component (in Twig) 2023-04-13 15:21:11 +02:00
denis.flaven@combodo.com
a1a9ffe192 Merge remote-tracking branch 'origin/feature/6133-add-extra-files-to-backup-and-restore' into saas/3.0 2023-04-06 10:51:29 +02:00
denis.flaven@combodo.com
001194835f Config file may not exist during unattended setup/backup 2023-04-06 10:50:47 +02:00
denis.flaven@combodo.com
7728082c00 Merge remote-tracking branch 'origin/feature/6133-add-extra-files-to-backup-and-restore' into saas/3.0 2023-04-04 15:55:09 +02:00
denis.flaven@combodo.com
955aefc05b Ignore non-existing files. 2023-04-04 14:26:31 +02:00
denis.flaven@combodo.com
970183ef45 Merge remote-tracking branch 'origin/feature/6132-disabling-tabs-dynamically' into saas/3.0 2023-04-03 14:02:19 +02:00
denis.flaven@combodo.com
6c2db1e687 Fixed tab activation afeter re-enabling. 2023-04-03 13:45:32 +02:00
denis.flaven@combodo.com
034ca26d01 Do NOT backup unsafe files. 2023-04-03 11:54:01 +02:00
denis.flaven@combodo.com
32d74fbc8e Merge branch 'feature/6132-disabling-tabs-dynamically' into saas/3.0 2023-03-30 17:08:02 +02:00
Molkobain
477f2f51e9 Update code to match conventions 2023-03-30 16:50:08 +02:00
denis.flaven@combodo.com
94ea8e60e8 Typo! 2023-03-30 14:29:29 +02:00
denis.flaven@combodo.com
b9a00b15f5 Disable tabs by ID instead of index
Disabled tabs are visible (with a 'not-allowed' cursor)  instead of being hidden from the extra tabs menu.
2023-03-30 14:16:26 +02:00
Denis
e87f5af465 Apply suggestions from code review
JS cleanup after review

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-03-30 13:25:55 +02:00
denis.flaven@combodo.com
97d717b016 Merge from support/3.0.2 2023-03-29 16:56:42 +02:00
denis.flaven@combodo.com
d03bd706e2 Support of extra files (configurable) in the backup. 2023-03-29 16:53:38 +02:00
denis.flaven@combodo.com
251fd3c67b N°6132 - disable tabs dynamically 2023-03-28 15:07:43 +02:00
odain
7b0a569c64 N°4762 - menu compilation: fix ci (merge issue) 2023-03-07 14:07:17 +01:00
odain
7176bc8686 N°4762 - menu compilation: fix broken menus scenario via AVA6 delta XML
N°4762-enhance test
2023-03-07 09:49:21 +01:00
Denis
9c0b906ded N°5922 - Fix plus button semantic on ext. key widget (#448)
* N°5922 - Enhance plus button on extkeywidget

* Properly reset the target class when closing the dialog

* Make icon buttons as actual clickable links for BeHat

* Apply suggestions from code review

Review by Guillaume. Thanks!

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-03-03 14:12:09 +01:00
odain
0533916dad 4762-menu compilation rework after brainstorming 2023-03-02 08:49:41 +01:00
odain
60b08586c2 N°4762-fix fresh install setup crash 2023-02-24 10:11:17 +01:00
odain
28df2942e4 N°6022 - Make synchro scripts work by http via token authentication with SYNCHRO scopes 2023-02-24 09:11:43 +01:00
odain
cd48d2ad37 N°4762 - Designer customization of menus moved in itop-structure crashs in iTop 3.0 2023-02-24 09:01:10 +01:00
odain
9db2205241 N°5891 - fix and enable tests 2023-02-16 16:59:43 +01:00
odain
045985cd5b 5891-renable badly named tests 2023-02-16 15:56:49 +01:00
odain
809b371520 N°5753 - add config parameter allow_rest_services_via_tokens to bypass rest secure profile option 2022-12-21 15:23:49 +01:00
odain
9bbc7342b8 enhance test framework: let AddProfileToUser work on any User not only UserLocal 2022-12-21 15:22:52 +01:00
odain
973c435138 Revert "N°5753 - exposer l'API Rest dans le SaaS - ugly way of passing API scope to rest.php during login"
This reverts commit 49748a0374.
2022-12-21 14:39:03 +01:00
denis.flaven@combodo.com
1c3dfd6491 Merge branch 5620 - also hide favorites orgs from preferences 2022-12-19 15:57:44 +01:00
denis.flaven@combodo.com
9400b697eb N°5620 Also hide the favorite orgs in preferences 2022-12-19 15:55:52 +01:00
odain
49748a0374 N°5753 - exposer l'API Rest dans le SaaS - ugly way of passing API scope to rest.php during login 2022-12-19 14:22:45 +01:00
odain
163276a6c2 N°5620-merge fix 2022-11-23 09:24:21 +01:00
odain
9b0c2f7324 Merge branch 'feature/5620-hide-org-filter-menu' into saas/3.0 2022-11-22 13:47:33 +01:00
odain
e1807f598f N°5620-fix ci 2022-11-22 13:46:33 +01:00
Molkobain
02e63fff64 Add PHPDoc 2022-11-22 13:28:02 +01:00
odain
0864f05d9f N°5620 - conf param renaming + backward compatibility test 2022-11-22 08:25:43 +01:00
denis.flaven@combodo.com
1bbcd9656a N°5619 - fixed crash when no provider at all! 2022-11-22 08:25:43 +01:00
odain
34d8e52c22 N°5620 - remove debug log 2022-11-22 08:25:27 +01:00
odain
ac7309e48c N°5620 - Hide the organization filter with a conf parameter 2022-11-22 08:25:27 +01:00
odain
c8fade6013 5620-simplify test to avoid regression in other test sections linked to MetaModel use 2022-11-22 07:42:03 +01:00
odain
ad052dd861 N°5620-renaming IsOrgMenuFilterAllowed<-IsSiloSelectionEnabled and made public 2022-11-22 07:31:12 +01:00
odain
a5ea868609 fix test 2022-11-21 10:41:03 +01:00
odain
0b03b3ef4d N°5620 - conf param renaming + backward compatibility test 2022-11-21 09:56:56 +01:00
odain
174cace20a N°5620 - remove debug log 2022-11-15 09:55:12 +01:00
odain
e3f5dbfc80 N°5620 - Hide the organization filter with a conf parameter 2022-11-15 09:49:36 +01:00
denis.flaven@combodo.com
40e24c25a2 N°5619 - hide newsroom menu when no provider 2022-11-15 09:17:27 +01:00
denis.flaven@combodo.com
d3f8e1c472 Revert "N°5619 - Hide newsroom menu when no provider"
This reverts commit 647b669eb9.
2022-11-14 18:28:18 +01:00
denis.flaven@combodo.com
647b669eb9 N°5619 - Hide newsroom menu when no provider 2022-11-14 18:18:10 +01:00
odain
c6e4466c53 ci: fix ItopDataTestCase CreateUser contactid unset 2022-10-26 14:03:11 +02:00
odain
6638eb4adc ci: adapt impersonate test to any friendlyname output 2022-10-26 09:50:56 +02:00
odain
eb40968e34 ci: add CreateContactlessUser method in test framework 2022-10-25 09:26:57 +02:00
odain-cbd
766c9f0e7e N°5305 - CSV import ergonomy PR (#332)
Reworked UI feedbacks on following attributes:
- enum
- date
- external key
2022-09-20 16:00:33 +02:00
67 changed files with 2994 additions and 570 deletions

View File

@@ -2184,5 +2184,50 @@ class RestUtils
*/
interface iModuleExtension
{
/**
* @api
*/
public function __construct();
}
/**
* Interface to provide messages to be displayed in the "Welcome Popup"
*
* @api
* @private
* @since 3.1.0
*/
interface iWelcomePopup
{
// 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;
}
/**
* 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;
}
}

View File

@@ -40,6 +40,36 @@
<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">

View File

@@ -9,6 +9,7 @@ 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");
/**
@@ -103,7 +104,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
@@ -273,12 +274,23 @@ 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' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
'aSubMenuNodes' => $aSubMenuNodes,
];
}
@@ -525,7 +537,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
@@ -533,7 +545,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();
@@ -643,7 +655,7 @@ abstract class MenuNode
/**
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
*/
*/
protected $m_aEnableStimuli;
/**
@@ -804,7 +816,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
@@ -977,7 +989,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)
@@ -1048,7 +1060,7 @@ class OQLMenuNode extends MenuNode
* @var bool|null
*/
protected $bSearchFormOpen;
/**
* Extra parameters to be passed to the display block to fine tune its appearence
*/
@@ -1081,7 +1093,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
@@ -1109,7 +1121,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
);
@@ -1343,10 +1355,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))
@@ -1355,7 +1367,7 @@ class NewObjectMenuNode extends MenuNode
break; // Enough for now
}
}
return $bActionIsAllowed;
return $bActionIsAllowed;
}
/**
@@ -1497,7 +1509,7 @@ class DashboardMenuNode extends MenuNode
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
}
}
}
/**
@@ -1538,7 +1550,7 @@ class ShortcutContainerMenuNode extends MenuNode
$sName = $this->GetMenuId().'_'.$oShortcut->GetKey();
new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++);
}
// Complete the tree
//
parent::PopulateChildMenus();

View File

@@ -1,29 +0,0 @@
<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>

View File

@@ -42,6 +42,17 @@ abstract class CellChangeSpec
return $this->m_sOql;
}
/**
* @since 3.1.0 N°5305
*/
public function GetDisplayableValueAndDescription(): string
{
return sprintf("%s%s",
$this->GetDisplayableValue(),
$this->GetDescription()
);
}
abstract public function GetDescription();
}
@@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify
parent::__construct($proposedValue, $previousValue);
}
public function GetDescription()
public function GetDisplayableValue()
{
if (is_null($this->m_proposedValue))
{
return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason);
return Dict::Format('UI:CSVReport-Value-SetIssue');
}
return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason);
return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue));
}
public function GetDescription()
{
return $this->m_sReason;
}
/*
* @since 3.1.0 N°5305
*/
public function GetDisplayableValueAndDescription(): string
{
return sprintf("%s. %s",
$this->GetDisplayableValue(),
$this->GetDescription()
);
}
}
class CellStatus_SearchIssue extends CellStatus_Issue
{
public function __construct()
/** @var string|null $m_sAllowedValues */
private $m_sAllowedValues;
/**
* @since 3.1.0 N°5305
* @var string $sSerializedSearch
*/
private $sSerializedSearch;
/** @var string|null $m_sTargetClass */
private $m_sTargetClass;
/**
* CellStatus_SearchIssue constructor.
* @since 3.1.0 N°5305
*
* @param string $sOql : main message
* @param string $sReason : main message
* @param null $sClass : used for additional message that provides allowed values for current class $sClass
* @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class
*/
public function __construct($sSerializedSearch, $sReason, $sClass=null, $sAllowedValues=null)
{
parent::__construct(null, null, null);
parent::__construct(null, null, $sReason);
$this->sSerializedSearch = $sSerializedSearch;
$this->m_sAllowedValues = $sAllowedValues;
$this->m_sTargetClass = $sClass;
}
public function GetDisplayableValue()
{
if (null === $this->m_sReason) {
return Dict::Format('UI:CSVReport-Value-NoMatch', '');
}
return $this->m_sReason;
}
public function GetDescription()
{
return Dict::S('UI:CSVReport-Value-NoMatch');
if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) ||
\utils::IsNullOrEmptyString($this->m_sTargetClass)) {
return '';
}
return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues);
}
/**
* @since 3.1.0 N°5305
* @return string
*/
public function GetSearchLinkUrl()
{
return sprintf("UI.php?operation=search&filter=%s",
rawurlencode($this->sSerializedSearch)
);
}
}
@@ -126,11 +201,24 @@ class CellStatus_NullIssue extends CellStatus_Issue
class CellStatus_Ambiguous extends CellStatus_Issue
{
protected $m_iCount;
/**
* @since 3.1.0 N°5305
* @var string
*/
protected $sSerializedSearch;
public function __construct($previousValue, $iCount, $sOql)
/**
* @since 3.1.0 N°5305
*
* @param $previousValue
* @param int $iCount
* @param string $sSerializedSearch
*
*/
public function __construct($previousValue, $iCount, $sSerializedSearch)
{
$this->m_iCount = $iCount;
$this->m_sQuery = $sOql;
$this->sSerializedSearch = $sSerializedSearch;
parent::__construct(null, $previousValue, '');
}
@@ -139,6 +227,17 @@ class CellStatus_Ambiguous extends CellStatus_Issue
$sCount = $this->m_iCount;
return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
}
/**
* @since 3.1.0 N°5305
* @return string
*/
public function GetSearchLinkUrl()
{
return sprintf("UI.php?operation=search&filter=%s",
rawurlencode($this->sSerializedSearch)
);
}
}
@@ -211,6 +310,26 @@ class RowStatus_Issue extends RowStatus
}
}
/**
* class dedicated to testability
* not used/ignored in csv imports UI/CLI
* @since 3.1.0 N°5305
*/
class RowStatus_Error extends RowStatus
{
/** @var string */
protected $m_sError;
public function __construct($sError)
{
$this->m_sError = $sError;
}
public function GetDescription()
{
return $this->m_sError;
}
}
/**
* BulkChange
@@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus
*/
class BulkChange
{
/** @var string */
protected $m_sClass;
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
// #@# todo: rename the variables to sColIndex
protected $m_aAttList; // attcode => iCol
protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
/** @var array<string, string> attcode as key, iCol as value */
protected $m_aAttList;
/** @var array<string, array<string, string>> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
protected $m_aExtKeys;
/** @var string[] list of attcode (attcode = 'id' for the pkey) */
protected $m_aReconcilKeys;
/** @var string OQL - if specified, then the missing items will be reported */
protected $m_sSynchroScope;
/**
* @var array<string, mixed> attcode as key, attvalue as value. Values to be set when an object gets out of scope
* (ignored if no scope has been defined)
*/
protected $m_aOnDisappear;
/**
* @see DateTime::createFromFormat
* @var string Date format specification
*/
protected $m_sDateFormat;
/**
* @see AttributeEnum
* @var boolean true if Values in the data set are localized
*/
protected $m_bLocalizedValues;
/** @var array Cache for resolving external keys based on the given search criterias */
protected $m_aExtKeysMappingCache;
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
{
@@ -266,25 +403,25 @@ class BulkChange
{
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol)
{
if ($sForeignAttCode == 'id')
if ($sReconKeyAttCode == 'id')
{
$value = (int) $aRowData[$iCol];
}
else
{
// The foreign attribute is one of our reconciliation key
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
}
$oExtObjects = new CMDBObjectSet($oReconFilter);
$aKeys = $oExtObjects->ToArray();
return array($oReconFilter->ToOql(), $aKeys);
return array($oReconFilter, $aKeys);
}
// Returns true if the CSV data specifies that the external key must be left undefined
@@ -321,7 +458,7 @@ class BulkChange
// External keys reconciliation
//
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
foreach($this->m_aExtKeys as $sAttCode => $aReconKeys)
{
// Skip external keys used for the reconciliation process
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
@@ -330,7 +467,7 @@ class BulkChange
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
{
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// Default reporting
// $aRowData[$iCol] is always null
@@ -352,25 +489,24 @@ class BulkChange
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
$aCacheKeys = array();
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// The foreign attribute is one of our reconciliation key
if ($sForeignAttCode == 'id')
if ($sReconKeyAttCode == 'id')
{
$value = $aRowData[$iCol];
}
else
{
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
$aCacheKeys[] = $value;
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
}
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
$iForeignKey = null;
$sOQL = '';
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
{
@@ -379,9 +515,8 @@ class BulkChange
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
{
// Cache hit
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
$iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
// Record the hit
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
}
@@ -389,34 +524,35 @@ class BulkChange
{
// Cache miss, let's initialize it
$oExtObjects = new CMDBObjectSet($oReconFilter);
$iCount = $oExtObjects->Count();
if ($iCount == 1)
$iObjectFoundCount = $oExtObjects->Count();
if ($iObjectFoundCount == 1)
{
$oForeignObj = $oExtObjects->Fetch();
$iForeignKey = $oForeignObj->GetKey();
}
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
'c' => $iCount,
'c' => $iObjectFoundCount,
'k' => $iForeignKey,
'oql' => $oReconFilter->ToOql(),
'h' => 0, // number of hits on this cache entry
);
}
switch($iCount)
switch($iObjectFoundCount)
{
case 0:
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
$aResults[$sAttCode]= new CellStatus_SearchIssue();
break;
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
$aResults[$sAttCode] = $oCellStatus_SearchIssue;
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
break;
case 1:
// Do change the external key attribute
$oTargetObj->Set($sAttCode, $iForeignKey);
break;
// Do change the external key attribute
$oTargetObj->Set($sAttCode, $iForeignKey);
break;
default:
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount);
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
}
}
@@ -433,7 +569,7 @@ class BulkChange
else
{
$aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
{
// Report the change on reconciliation values as well
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
@@ -479,9 +615,18 @@ class BulkChange
}
} else {
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) {
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
} else {
if (is_null($value) && (strlen($aRowData[$iCol]) > 0))
{
if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){
/** @var AttributeDefinition $oAttributeDefinition */
$oAttributeDefinition = $oAttDef;
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues()));
} else {
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
}
}
else
{
$res = $oTargetObj->CheckValue($sAttCode, $value);
if ($res === true)
{
@@ -559,6 +704,95 @@ class BulkChange
return $aResults;
}
/**
* search with current permissions did not match
* let's search why and give some more feedbacks to the user through proper labels
*
* @param DBObjectSearch $oDbSearchWithConditions search used to find external key
*
* @return \CellStatus_SearchIssue
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
*
* @since 3.1.0 N°5305
*/
protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue {
//current search with current permissions did not match
//let's search why and give some more feedback to the user
$sSerializedSearch = $oDbSearchWithConditions->serialize();
// Count all objects with all permissions without any condition
$oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass());
$oDbSearchWithoutAnyCondition->AllowAllData(true);
$oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
$iAllowAllDataObjectCount = $oExtObjectSet->Count();
if ($iAllowAllDataObjectCount === 0) {
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
// Count all objects with current user permissions
$oDbSearchWithoutAnyCondition->AllowAllData(false);
$oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
$iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count();
if ($iCurrentUserRightsObjectCount === 0){
// No objects visible by current user
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
try{
$aDisplayedAllowedValues = [];
// Possibles values are displayed to UI user. we have to limit the amount of displayed values
$oExtObjectSetWithCurrentUserPermissions->SetLimit(4);
for($i = 0; $i < 3; $i++){
/** @var \DBObject $oVisibleObject */
$oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch();
if (is_null($oVisibleObject)){
break;
}
$aCurrentAllowedValueFields = [];
foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){
$aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode);
}
$aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields);
}
$allowedValues = implode(", ", $aDisplayedAllowedValues);
if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){
$allowedValues .= "...";
}
} catch(Exception $e) {
IssueLog::Error("failure during CSV import when fetching few visible objects: ", null,
[ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()]
);
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
}
if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) {
// No match and some objects NOT visible by current user. including current search maybe...
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass());
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
}
// No match. This is not linked to any right issue
// Possible values: DD,DD
$aCurrentValueFields = [];
foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){
$aCurrentValueFields[] = $sValue;
}
$value =implode(" ", $aCurrentValueFields);
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value);
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
}
protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
{
$aResults = array();
@@ -670,6 +904,8 @@ class BulkChange
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return $oTargetObj;
}
@@ -736,6 +972,8 @@ class BulkChange
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return;
}
@@ -785,6 +1023,8 @@ class BulkChange
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
//__ERRORS__ used by tests only
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
return;
}
@@ -872,14 +1112,18 @@ class BulkChange
$sFormat = $sDateFormat;
}
$oFormat = new DateTimeFormat($sFormat);
$sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
$sRegExp = $oFormat->ToRegExpr('/');
if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol]))
$sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample);
if (!preg_match($sRegExp, $sValue))
{
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg);
}
else
{
$oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]);
$oDate = DateTime::createFromFormat($sFormat, $sValue);
if ($oDate !== false)
{
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
@@ -889,7 +1133,7 @@ class BulkChange
{
// Leave the cell unchanged
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, utils::HtmlEntities($this->m_aData[$iRow][$iCol]), Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
$aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
}
}
}
@@ -943,7 +1187,9 @@ class BulkChange
else
{
// The value has to be found or verified
list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
/** var DBObjectSearch $oReconFilter */
list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
if (count($aMatches) == 1)
{
@@ -953,11 +1199,12 @@ class BulkChange
}
elseif (count($aMatches) == 0)
{
$aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
$aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
}
else
{
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
}
}
}
@@ -1010,7 +1257,7 @@ class BulkChange
default:
// Found several matches, ambiguous
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
$aResult[$iRow]["finalclass"]= 'n/a';
}
}

View File

@@ -1257,6 +1257,22 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'navigation_menu.show_organization_filter' => [
'type' => 'bool',
'description' => 'Display organization filter in menu',
'default' => true,
'value' => true,
'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',
@@ -1888,7 +1904,7 @@ class Config
}
if (strlen($sNoise) > 0)
{
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
throw new ConfigException('Syntax error in configuration file',
array('file' => $sConfigFile, 'error' => '<tt>'.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'</tt>'));
}
@@ -2698,7 +2714,7 @@ class ConfigPlaceholdersResolver
}
$sPattern = '/\%(env|server)\((\w+)\)(?:\?:(\w*))?\%/'; //3 capturing groups, ie `%env(HTTP_PORT)?:8080%` produce: `env` `HTTP_PORT` and `8080`.
if (! preg_match_all($sPattern, $rawValue, $aMatchesCollection, PREG_SET_ORDER))
{
return $rawValue;

View File

@@ -34,11 +34,6 @@ tr.ibo-csv-import--row-unchanged td {
border-bottom: 1px $ibo-csv-import--row--border-color solid;
}
.wizContainer table tr.ibo-csv-import--row-error td {
border-bottom: 1px $ibo-csv-import--row--border-color solid;
background-color: $ibo-csv-import--row-error--background-color;
}
tr.ibo-csv-import--row-modified td {
border-bottom: 1px $ibo-csv-import--row--border-color solid;
}
@@ -51,4 +46,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;
}
}

View File

@@ -17,7 +17,9 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
#welcome_popup{
display: flex;
}
.ibo-welcome-popup--columns{
display: flex;
}
.ibo-welcome-popup--image{
display: flex;
@@ -44,7 +46,39 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
}
}
}
.ibo-welcome-popup--text--options{
position: absolute;
bottom: $ibo-welcome-popup--text--options--bottom;
.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;
}
}

View File

@@ -35,6 +35,10 @@ $ibo-sticky-sentinel-bottom--height: $ibo-sticky-sentinel--height !default;
opacity: 1 !important; /* Note: !important is necessary as it needs to overload any standard rules */
}
.ibo-is-disabled {
cursor: not-allowed !important; /* Note: !important is necessary as it needs to overload any standard rules */
}
/****************************/
/* Disposition / alignement */
/****************************/

File diff suppressed because one or more lines are too long

View File

@@ -187,6 +187,12 @@ class DBRestore extends DBBackup
@chmod($sConfigFile, 0770); // Allow overwriting the file
rename($sDataDir.'/config-itop.php', $sConfigFile);
@chmod($sConfigFile, 0440); // Read-only
$aExtraFiles = $this->ListExtraFiles($sDataDir);
foreach($aExtraFiles as $sSourceFilePath => $sDestinationFilePath) {
SetupUtils::builddir(dirname($sDestinationFilePath));
rename($sSourceFilePath, $sDestinationFilePath);
}
try {
SetupUtils::rrmdir($sDataDir);
@@ -211,4 +217,27 @@ class DBRestore extends DBBackup
$oRestoreMutex->Unlock();
}
}
/**
* List the 'extra files' found in the decompressed archive
* (i.e. files other than config-itop.php, delta.xml, itop-dump.sql or production-modules/*
* @param string $sDataDir
* @return string[]
*/
protected function ListExtraFiles(string $sDataDir)
{
$aExtraFiles = [];
$aStandardFiles = ['config-itop.php', 'itop-dump.sql', 'production-modules', 'delta.xml'];
$oDirectoryIterator = new RecursiveDirectoryIterator($sDataDir, FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS);
$oIterator = new RecursiveIteratorIterator($oDirectoryIterator);
foreach ($oIterator as $oFileInfo)
{
if (in_array($oFileInfo->getFilename(), $aStandardFiles)) continue;
if (strncmp($oFileInfo->getPathname(), $sDataDir.'/production-modules', strlen($sDataDir.'/production-modules')) == 0) continue;
$aExtraFiles[$oFileInfo->getPathname()] = APPROOT.substr($oFileInfo->getPathname(), strlen($sDataDir));
}
return $aExtraFiles;
}
}

View File

@@ -644,9 +644,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ',
'UI:CSVReport-Value-Modified' => 'Upraveno',
'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Žádná shoda',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
'UI:CSVReport-Row-Unchanged' => 'nezměněn',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ',
'UI:CSVReport-Value-Modified' => 'Ændret',
'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No match',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
'UI:CSVReport-Row-Unchanged' => 'Uændret',

View File

@@ -365,6 +365,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
We hope youll 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, verwerfen Sie diese Nachricht',
'UI:WelcomeMenu:AllOpenRequests' => 'Offene Requests: %1$d',
'UI:WelcomeMenu:MyCalls' => 'An mich gestellte Benutzeranfragen',
'UI:WelcomeMenu:OpenIncidents' => 'Offene Incidents: %1$d',
@@ -633,9 +634,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ',
'UI:CSVReport-Value-Modified' => 'Modifiziert',
'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Kein Treffer',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
'UI:CSVReport-Row-Unchanged' => 'Unverändert',

View File

@@ -381,6 +381,7 @@ Dict::Add('EN US', 'English', 'English', array(
We hope youll 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',
@@ -650,9 +651,14 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ',
'UI:CSVReport-Value-Modified' => 'Modified',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No match',
'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'',
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Some possible \'%1$s\' value(s): %2$s',
'UI:CSVReport-Value-NoMatch-NoObject' => 'There are no \'%1$s\' objects',
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'There are no \'%1$s\' objects found with your current profile',
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'There are some \'%1$s\' objects not visible with your current profile',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
'UI:CSVReport-Row-Unchanged' => 'unchanged',
@@ -666,11 +672,13 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)',
'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s',
'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling',
'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s',
'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s',
'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s',
'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)',
'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s',
'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format',
'UI:CSVReport-Row-Issue-ExpectedDateFormat' => 'Expected format: %1$s',
'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile',
'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation',
'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s',

View File

@@ -377,6 +377,7 @@ 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',
@@ -644,9 +645,9 @@ Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ',
'UI:CSVReport-Value-Modified' => 'Modificado',
'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s',
'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
'UI:CSVReport-Row-Unchanged' => 'Sin Cambios',

View File

@@ -365,6 +365,7 @@ 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',
@@ -633,9 +634,14 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ',
'UI:CSVReport-Value-Modified' => 'Modifié',
'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s',
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance',
'UI:CSVReport-Value-SetIssue' => 'Valeur invalide',
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'',
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance avec \'%1$s\'',
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Valeur(s) possible(s) pour l\'objet \'%1$s\' : %2$s',
'UI:CSVReport-Value-NoMatch-NoObject' => 'Il n\'y a aucun objet \'%1$s\'',
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'Il n\'y a aucun objet \'%1$s\' visible par votre utilisateur',
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'Il existe des objet(s) \'%1$s\' non visible(s) par votre utilisateur',
'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire',
'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
'UI:CSVReport-Row-Unchanged' => 'inchangé',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:',
'UI:CSVReport-Value-Modified' => 'Modified~~',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
'UI:CSVReport-Value-NoMatch' => 'No match~~',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',

View File

@@ -644,9 +644,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ',
'UI:CSVReport-Value-Modified' => 'Modified~~',
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
'UI:CSVReport-Value-NoMatch' => 'No match~~',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',

View File

@@ -633,9 +633,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。',
'UI:CSVReport-Value-Modified' => '修正済み',
'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s',
'UI:CSVReport-Value-NoMatch' => 'マッチしません',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => '必須の値がありません',
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
'UI:CSVReport-Row-Unchanged' => '未変更',

View File

@@ -644,9 +644,9 @@ We hopen dat je even hard van deze versie geniet als dat we zelf ervan hebben ge
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ',
'UI:CSVReport-Value-Modified' => 'Aangepast',
'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Geen match',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
'UI:CSVReport-Row-Unchanged' => 'onveranderd',

View File

@@ -643,9 +643,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ',
'UI:CSVReport-Value-Modified' => 'Zmodyfikowano',
'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Nie pasuje',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
'UI:CSVReport-Row-Unchanged' => 'niezmieniony',

View File

@@ -644,9 +644,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ',
'UI:CSVReport-Value-Modified' => 'Modificado',
'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - razão: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - razão: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Não combina',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
'UI:CSVReport-Row-Unchanged' => 'unchanged',

View File

@@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
'UI:CSVReport-Value-Modified' => 'Изменен',
'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Нет совпадений',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
'UI:CSVReport-Row-Unchanged' => 'без изменений',

View File

@@ -634,9 +634,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ',
'UI:CSVReport-Value-Modified' => 'Upravený',
'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s',
'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
'UI:CSVReport-Row-Unchanged' => 'Nezmený',

View File

@@ -661,9 +661,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ',
'UI:CSVReport-Value-Modified' => 'Değiştiridi',
'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s',
'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi',

View File

@@ -649,9 +649,9 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ',
'UI:CSVReport-Value-Modified' => '已修改',
'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s',
'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s',
'UI:CSVReport-Value-NoMatch' => '不匹配',
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
'UI:CSVReport-Value-Missing' => '缺少必填项',
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
'UI:CSVReport-Row-Unchanged' => '保持不变',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -124,5 +124,16 @@ $.widget( "itop.regulartabs", $.ui.tabs, {
this._off( prevPanels.not( this.panels ) );
}
},
});
// JQuery UI overload
disable: function(index){
const panel = this._getPanelForTab( index );
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
this._super( index );
},
// JQuery UI overload
enable: function(index) {
const panel = this._getPanelForTab( index );
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
this._super( index );
},
});

View File

@@ -377,4 +377,16 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
setTab : function(tab){
this.active = tab;
},
});
// JQuery UI overload
disable: function(index){
const panel = this._getPanelForTab( this.tabs[index] );
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
this._super( index );
},
// JQuery UI overload
enable: function(index) {
const panel = this._getPanelForTab( this.tabs[index] );
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
this._super( index );
},
});

View File

@@ -15,6 +15,7 @@ $(function()
css_classes:
{
is_hidden: 'ibo-is-hidden',
is_disabled: 'ibo-is-disabled',
is_transparent: 'ibo-is-transparent',
is_opaque: 'ibo-is-opaque',
is_scrollable: 'ibo-is-scrollable',
@@ -252,6 +253,11 @@ $(function()
// Prevent anchor default behaviour
oEvent.preventDefault();
if (oExtraTabTogglerElem.attr('aria-disabled') === 'true') {
// Corresponding tab is disabled, do nothing
oEvent.stopPropagation();
return;
}
// Trigger click event on real tab toggler (the hidden one)
const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
@@ -297,17 +303,30 @@ $(function()
const sTabId = oTabHeaderElem.attr('data-tab-id');
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
// Disabled tabs should be disabled in the ExtraTabs list as well
let bIsDisabled = false;
if (oTabHeaderElem.attr('aria-disabled') === 'true') {
bIsDisabled = true;
}
// Manually check if the tab header is visible if the info isn't passed
if (bIsVisible === null) {
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
}
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
}
// Hide/show the corresponding extra tab element
if (bIsVisible) {
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
} else {
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
}
if (bIsVisible) {
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
} else {
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
}
// Enable/disable the corresponding extra tab element
if (bIsDisabled) {
oMatchingExtraTabElem.attr('aria-disabled', 'true');
oMatchingExtraTabElem.addClass(this.css_classes.is_disabled);
} else {
oMatchingExtraTabElem.attr('aria-disabled', 'false');
oMatchingExtraTabElem.removeClass(this.css_classes.is_disabled);
}
},
// - Update extra tabs list
_updateExtraTabsList: function () {
@@ -326,7 +345,7 @@ $(function()
* @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
* @private
*/
_getTabIdFromTabIndex(iIdx) {
_getTabIdFromTabIndex: function(iIdx) {
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
},
/**
@@ -334,10 +353,41 @@ $(function()
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
* @private
*/
_getTabIndexFromTabId(sId) {
_getTabIndexFromTabId: function(sId) {
const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
}
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*
* @private
*/
_getTabElementFromTabId: function(sId) {
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*/
disableTab: function(sId){
const tabsWidget = this.GetTabsWidget();
const iIdx = this._getTabIndexFromTabId(sId);
tabsWidget.disable(iIdx);
const tabElement = this._getTabElementFromTabId(sId);
this._updateTabHeaderDisplay(tabElement);
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*/
enableTab: function(sId){
const tabsWidget = this.GetTabsWidget();
const iIdx = this._getTabIndexFromTabId(sId);
tabsWidget.enable(iIdx);
const tabElement = this._getTabElementFromTabId(sId);
this._updateTabHeaderDisplay(tabElement);
}
});
});

View File

@@ -14,6 +14,7 @@ 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',
@@ -142,6 +143,7 @@ return array(
'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php',
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/application/Branding.php',
'Combodo\\iTop\\Application\\DatamodelDisplayService' => $baseDir . '/sources/application/DatamodelDisplayService.php',
'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/application/Helper/Session.php',
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/application/Helper/WebResourcesHelper.php',
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => $baseDir . '/sources/application/search/ajaxsearchexception.class.inc.php',
@@ -296,11 +298,14 @@ return array(
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => $baseDir . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.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\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
'Combodo\\iTop\\Controller\\WelcomePopupController' => $baseDir . '/sources/Controller/WelcomePopupController.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',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
@@ -1398,6 +1403,7 @@ return array(
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Error' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Modify' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_NewObj' => $baseDir . '/core/bulkchange.class.inc.php',
@@ -2973,6 +2979,7 @@ return array(
'iTopStandardURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
'iTopWebPage' => $baseDir . '/sources/application/WebPage/iTopWebPage.php',
'iTopWizardWebPage' => $baseDir . '/sources/application/WebPage/iTopWizardWebPage.php',
'iWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
'iWorkingTimeComputer' => $baseDir . '/core/computing.inc.php',
'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php',
'ormCaseLog' => $baseDir . '/core/ormcaselog.class.inc.php',

View File

@@ -382,6 +382,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'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',
@@ -510,6 +511,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php',
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/application/Branding.php',
'Combodo\\iTop\\Application\\DatamodelDisplayService' => __DIR__ . '/../..' . '/sources/application/DatamodelDisplayService.php',
'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/application/Helper/Session.php',
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/application/Helper/WebResourcesHelper.php',
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => __DIR__ . '/../..' . '/sources/application/search/ajaxsearchexception.class.inc.php',
@@ -664,11 +666,14 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => __DIR__ . '/../..' . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.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\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
'Combodo\\iTop\\Controller\\WelcomePopupController' => __DIR__ . '/../..' . '/sources/Controller/WelcomePopupController.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',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
@@ -1766,6 +1771,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Error' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Modify' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_NewObj' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
@@ -3341,6 +3347,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'iTopStandardURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
'iTopWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWebPage.php',
'iTopWizardWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWizardWebPage.php',
'iWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iWorkingTimeComputer' => __DIR__ . '/../..' . '/core/computing.inc.php',
'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
'ormCaseLog' => __DIR__ . '/../..' . '/core/ormcaselog.class.inc.php',

View File

@@ -19,6 +19,7 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
/**
* Displays a popup welcome message, once per session at maximum
@@ -28,18 +29,17 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
*
* @return void
*/
function DisplayWelcomePopup(WebPage $oP)
function DisplayWelcomePopup(WebPage $oP): void
{
if (!Session::IsSet('welcome'))
{
// 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)
$oWelcomePopupService = new WelcomePopupService();
$aMessages = $oWelcomePopupService->GetMessages();
if (count($aMessages) > 0)
{
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup');
Session::Set('welcome', 'ok');
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup', ['messages' => $aMessages]);
}
Session::Set('welcome', 'ok'); // Try just once per session
}
}

View File

@@ -14,6 +14,7 @@ use Combodo\iTop\Controller\Base\Layout\ActivityPanelController;
use Combodo\iTop\Controller\PreferencesController;
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
use Combodo\iTop\Controller\WelcomePopupController;
require_once('../approot.inc.php');
@@ -2690,13 +2691,31 @@ 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;
default:
$oPage->p("Invalid query.");
}
$oKPI->ComputeAndReport('Data fetch and format');
$oPage->output();
} catch (Exception $e)
{
} catch (Exception $e) {
// note: transform to cope with XSS attacks
echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());

View File

@@ -139,7 +139,7 @@ try {
}
return $aResult;
}
/**
* Return the most frequent (and regularly occuring) character among the given set, in the specified lines
* @param array $aCSVData The input data, one entry per line
@@ -175,7 +175,7 @@ try {
}
$iLine++;
}
$aScores = array();
foreach($aGuesses as $sSep => $aData)
{
@@ -186,7 +186,7 @@ try {
$sSeparator = $aKeys[0]; // Take the first key, the one with the best score
return $sSeparator;
}
/**
* Try to predict the CSV parameters based on the input data
* @param string $sCSVData The input data
@@ -197,10 +197,10 @@ try {
$aData = explode("\n", $sCSVData);
$sSeparator = GuessFromFrequency($aData, array("\t", ',', ';', '|')); // Guess the most frequent (and regular) character on each line
$sQualifier = GuessFromFrequency($aData, array('"', "'")); // Guess the most frequent (and regular) character on each line
return array('separator' => $sSeparator, 'qualifier' => $sQualifier);
}
/**
* Display a banner for the special "synchro" mode
* @param WebPage $oP The Page for the output
@@ -216,6 +216,7 @@ try {
* Add a paragraph to the body of the page
*
* @param string $s_html
* @param ?string $sLinkUrl
*
* @return string
*/
@@ -260,9 +261,9 @@ try {
$sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data');
$sDateTimeFormat = utils::ReadParam('date_time_format', 'default');
$sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data');
$sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat;
if (!empty($sSynchroScope))
{
$oSearch = DBObjectSearch::FromOQL($sSynchroScope);
@@ -277,7 +278,7 @@ try {
$sSynchroScope = '';
$aSynchroUpdate = null;
}
// Parse the data set
$oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
$aData = $oCSVParser->ToArray($iSkippedLines);
@@ -287,10 +288,10 @@ try {
$aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error
$iRealSkippedLines++;
}
// Format for the line numbers
$sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3
// Compute the list of search/reconciliation criteria
$aSearchKeys = array();
foreach($aSearchFields as $index => $sDummy)
@@ -304,16 +305,16 @@ try {
}
else
{
$aSearchKeys[$sSearchField] = '';
$aSearchKeys[$sSearchField] = '';
}
if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField))
{
// Remove invalid or unmapped search fields
$aSearchFields[$index] = null;
unset($aSearchKeys[$sSearchField]);
unset($aSearchKeys[$sSearchField]);
}
}
// Compute the list of fields and external keys to process
$aExtKeys = array();
$aAttributes = array();
@@ -346,13 +347,13 @@ try {
}
else
{
$aAttributes[$sAttCode] = $iIndex;
$aAttributes[$sAttCode] = $iIndex;
}
}
}
}
}
}
$oMyChange = null;
if (!$bSimulate)
{
@@ -362,8 +363,6 @@ try {
CMDBObject::SetTrackOrigin(CMDBChangeOrigin::CSV_INTERACTIVE);
$oMyChange = CMDBObject::GetCurrentChange();
}
CMDBObject::SetTrackOrigin('csv-interactive');
$oBulk = new BulkChange(
$sClassName,
$aData,
@@ -373,7 +372,7 @@ try {
empty($sSynchroScope) ? null : $sSynchroScope,
$aSynchroUpdate,
$sChosenDateFormat, // date format
true // localize
true // localize
);
$oBulk->SetReportHtml();
@@ -440,7 +439,6 @@ try {
case 'RowStatus_NewObj':
$iCreated++;
$sFinalClass = $aResRow['finalclass'];
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
$sCSSRowClass = 'ibo-csv-import--row-added';
if ($bSimulate) {
@@ -456,7 +454,7 @@ try {
case 'RowStatus_Issue':
$iErrors++;
$sMessage .= GetDivAlert($oStatus->GetDescription());
$sStatus = '<img src="../images/error.png" title="'.Dict::S('UI:CSVReport-Icon-Error').'">';//translate
$sStatus = '<div class="ibo-csv-import--cell-error"><i class="fas fa-exclamation-triangle" title="'.Dict::S('UI:CSVReport-Icon-Error').'" /></div>';//translate
$sCSSMessageClass = 'ibo-csv-import--cell-error';
$sCSSRowClass = 'ibo-csv-import--row-error';
if (array_key_exists($iLine, $aData)) {
@@ -477,33 +475,36 @@ try {
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
switch (get_class($oExtKeyCellStatus)) {
case 'CellStatus_Issue':
case 'CellStatus_SearchIssue':
case 'CellStatus_NullIssue':
case 'CellStatus_Ambiguous':
$sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription());
break;
default:
// Do nothing
}
$oCellStatus = $oExtKeyCellStatus;
}
$sHtmlValue = $oCellStatus->GetDisplayableValue();
switch (get_class($oCellStatus)) {
case 'CellStatus_Issue':
case 'CellStatus_NullIssue':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
break;
case 'CellStatus_SearchIssue':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">ERROR: '.$sHtmlValue.$sCellMessage.'</div>';
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
'<a href="',
$oCellStatus->GetSearchLinkUrl(),
'"><div class="ibo-csv-import--cell-error">',
Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue),
GetDivAlert($oCellStatus->GetDescription()),
'<i class="fas fa-search"></i></div><a/>'
);
break;
case 'CellStatus_Ambiguous':
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error" >'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'</div>';
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
'<a href="',
$oCellStatus->GetSearchLinkUrl(),
'"><i class="fas fa-search"/><div class="ibo-csv-import--cell-error">',
Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue),
GetDivAlert($oCellStatus->GetDescription()),
'<i class="fas fa-search"></i></div><a/>'
);
break;
case 'CellStatus_Modify':
@@ -592,7 +593,7 @@ try {
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
$oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})");
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<img src="../images/error.png">&nbsp;'.sprintf($aDisplayFilters['errors'], $iErrors), '', "1", "show_errors", "checkbox");
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<i class="fas fa-exclamation-triangle" style="color:#A33; background-color: #FFF0F0;">&nbsp;'.sprintf($aDisplayFilters['errors'], $iErrors).'</i></i>', '', "1", "show_errors", "checkbox");
$oCheckBoxUnchanged->GetInput()->SetIsChecked(true);
$oCheckBoxUnchanged->SetBeforeInput(false);
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
@@ -679,7 +680,7 @@ try {
EOF
);
}
$sErrors = json_encode(Dict::Format('UI:CSVImportError_items', $iErrors));
$sCreated = json_encode(Dict::Format('UI:CSVImportCreated_items', $iCreated));
$sModified = json_encode(Dict::Format('UI:CSVImportModified_items', $iModified));
@@ -774,7 +775,7 @@ EOF
{
return null;
}
}
/**
* Perform the actual load of the CSV data and display the results
@@ -798,7 +799,7 @@ EOF
$oField->AddSubBlock($oText);
}
}
/**
* Simulate the load of the CSV data and display the results
* @param WebPage $oPage The web page to display the wizard
@@ -810,7 +811,7 @@ EOF
$oPage->AddSubBlock($oPanel);
ProcessCSVData($oPage, true /* simulate */);
}
/**
* Select the mapping between the CSV column and the fields of the objects
* @param WebPage $oPage The web page to display the wizard
@@ -923,10 +924,10 @@ EOF
$aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name');
$sFieldsMapping = addslashes(json_encode($aFieldsMapping));
$sSearchFields = addslashes(json_encode($aSearchFields));
$oPage->add_ready_script("DoMapping('$sFieldsMapping', '$sSearchFields');"); // There is already a class selected, run the mapping
}
$oPage->add_script(
<<<EOF
var aDefaultKeys = new Array();
@@ -1142,7 +1143,7 @@ EOF
EOF
);
}
/**
* Select the options of the CSV load and check for CSV parsing errors
* @param WebPage $oPage The current web page
@@ -1166,7 +1167,7 @@ EOF
$sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data');
}
$sEncoding = utils::ReadParam('encoding', 'UTF-8');
// Compute a subset of the data set, now that we know the charset
if ($sEncoding == 'UTF-8')
{
@@ -1183,7 +1184,7 @@ EOF
{
$sUTF8Data = iconv($sEncoding, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
}
$aGuesses = GuessParameters($sUTF8Data); // Try to predict the parameters, based on the input data
$iSkippedLines = utils::ReadParam('nb_skipped_lines', '');
@@ -1191,7 +1192,7 @@ EOF
$sTextQualifier = utils::ReadParam('text_qualifier', '', false, 'raw_data');
if ($sTextQualifier == '') // May be set to an empty value by the previous page
{
$sTextQualifier = $aGuesses['qualifier'];
$sTextQualifier = $aGuesses['qualifier'];
}
$sOtherTextQualifier = in_array($sTextQualifier, array('"', "'")) ? '' : $sTextQualifier;
$bHeaderLine = utils::ReadParam('header_line', 0);
@@ -1610,7 +1611,7 @@ EOF
null, AjaxTab::ENUM_TAB_PLACEHOLDER_MISC);
}
}
switch($iStep)
{
case 11:
@@ -1618,45 +1619,45 @@ EOF
$oPage = new AjaxPage('');
BulkChange::DisplayImportHistory($oPage);
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tableHover();');
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
break;
case 10:
// Case generated by BulkChange::DisplayImportHistory
$iChange = (int)utils::ReadParam('changeid', 0);
BulkChange::DisplayImportHistoryDetails($oPage, $iChange);
break;
case 5:
LoadData($oPage);
break;
case 4:
Preview($oPage);
break;
case 3:
SelectMapping($oPage);
break;
case 2:
SelectOptions($oPage);
break;
case 1:
case 6: // Loop back here when we are done
default:
Welcome($oPage);
}
$oPage->output();
}
catch(CoreException $e)
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
$oP->output();
if (MetaModel::IsLogEnabledIssue())
@@ -1684,8 +1685,8 @@ catch(Exception $e)
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
$oP->output();
if (MetaModel::IsLogEnabledIssue())
@@ -1705,4 +1706,4 @@ catch(Exception $e)
IssueLog::Error($e->getMessage());
}
}
}

View File

@@ -139,55 +139,58 @@ JS
//
//////////////////////////////////////////////////////////////////////////
$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);
$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);
$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
// );
//
// }
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
// 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);
}
//////////////////////////////////////////////////////////////////////////
//
// Shortcuts

View File

@@ -205,11 +205,12 @@ class DBBackup
*
* @param string $sSourceConfigFile
* @param string $sTmpFolder
* @param bool $bSkipSQLDumpForTesting
*
* @return array list of files to archive
* @throws \Exception
*/
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder)
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder, $bSkipSQLDumpForTesting = false)
{
$aRet = array();
if (is_dir($sTmpFolder))
@@ -226,7 +227,7 @@ class DBBackup
{
$sFile = $sTmpFolder.'/config-itop.php';
$this->LogInfo("backup: adding resource '$sSourceConfigFile'");
copy($sSourceConfigFile, $sFile);
@copy($sSourceConfigFile, $sFile); // During unattended install config file may be absent
$aRet[] = $sFile;
}
@@ -247,9 +248,41 @@ class DBBackup
SetupUtils::copydir($sExtraDir, $sFile);
$aRet[] = $sFile;
}
$sDataFile = $sTmpFolder.'/itop-dump.sql';
$this->DoBackup($sDataFile);
$aRet[] = $sDataFile;
if (MetaModel::GetConfig() !== null) // During unattended install config file may be absent
{
$aExtraFiles = MetaModel::GetModuleSetting('itop-backup', 'extra_files', []);
foreach($aExtraFiles as $sExtraFileOrDir)
{
if(!file_exists(APPROOT.'/'.$sExtraFileOrDir)) continue; // Ignore non-existing files
$sExtraFullPath = realpath(APPROOT.'/'.$sExtraFileOrDir);
if (strncmp(APPROOT, $sExtraFullPath, strlen(APPROOT)) !== 0)
{
throw new Exception("Backup: Aborting, resource '$sExtraFileOrDir'. Considered as UNSAFE because not inside the iTop directory.");
}
if (is_dir($sExtraFullPath))
{
$sFile = $sTmpFolder.'/'.$sExtraFileOrDir;
$this->LogInfo("backup: adding directory '$sExtraFileOrDir'");
SetupUtils::copydir($sExtraFullPath, $sFile);
$aRet[] = $sFile;
}
elseif (file_exists($sExtraFullPath))
{
$sFile = $sTmpFolder.'/'.$sExtraFileOrDir;
$this->LogInfo("backup: adding file '$sExtraFileOrDir'");
@mkdir(dirname($sFile), 0755, true);
copy($sExtraFullPath, $sFile);
$aRet[] = $sFile;
}
}
}
if (!$bSkipSQLDumpForTesting)
{
$sDataFile = $sTmpFolder.'/itop-dump.sql';
$this->DoBackup($sDataFile);
$aRet[] = $sDataFile;
}
return $aRet;
}

View File

@@ -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;
@@ -315,7 +315,7 @@ class MFCompiler
apc_clear_cache();
}
}
/**
* Perform the actual "Compilation" of all modules
@@ -327,21 +327,16 @@ class MFCompiler
*/
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
{
$aAllClasses = array(); // flat list of classes
$aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string)
$aAllClasses = []; // flat list of classes
$aModulesInfo = []; // 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;
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
$aMenusByModule[$sModuleMenu][] = $sMenuId;
}
/**
* @since 3.1 N°4762
*/
$oParentMenuNodeCompiler = new ParentMenuNodeCompiler($this);
$oParentMenuNodeCompiler->LoadXmlMenus($this->oFactory);
// 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
@@ -386,6 +381,7 @@ class MFCompiler
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
foreach ($aModules as $foo => $oModule) {
$sModuleName = $oModule->GetName();
$sModuleVersion = $oModule->GetVersion();
@@ -418,7 +414,7 @@ class MFCompiler
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
}
}
if (array_key_exists($sModuleName, $this->aSnippets))
{
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
@@ -461,7 +457,7 @@ class MFCompiler
}
}
if (!array_key_exists($sModuleName, $aMenusByModule))
if (is_null($oParentMenuNodeCompiler->GetMenusByModule($sModuleName)))
{
$this->Log("Found module without menus declared: $sModuleName");
}
@@ -481,75 +477,19 @@ class $sMenuCreationClass extends ModuleHandlerAPI
global \$__comp_menus__; // ensure that the global variable is indeed global !
EOF;
// Preliminary: determine parent menus not defined within the current module
$aMenusToLoad = array();
$aParentMenus = array();
foreach($aMenusByModule[$sModuleName] as $sMenuId)
{
$oMenuNode = $aMenuNodes[$sMenuId];
if ($sParent = $oMenuNode->GetChildText('parent', null))
{
$aMenusToLoad[] = $sParent;
$aParentMenus[] = $sParent;
}
// 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
{
$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);
}
}
$oParentMenuNodeCompiler->CompileModuleMenus($oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
$sIndent = "\t\t";
foreach ($aMenuLinesForAll as $sPHPLine)
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAll() as $sPHPLine)
{
$sCompiledCode .= $sIndent.$sPHPLine."\n";
}
if (count($aMenuLinesForAdmins) > 0)
if (count($oParentMenuNodeCompiler->GetMenuLinesForAdmins()) > 0)
{
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
$sCompiledCode .= $sIndent."{\n";
foreach ($aMenuLinesForAdmins as $sPHPLine)
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAdmins() as $sPHPLine)
{
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
}
@@ -581,7 +521,7 @@ EOF;
$sCompiledCode .= $aSnippet['content']."\n";
}
}
// Create (overwrite if existing) the compiled file
//
if (strlen($sCompiledCode) > 0)
@@ -625,7 +565,7 @@ EOF;
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
}
} // foreach module
// Compile the dictionaries -out of the modules
//
$sDictDir = $sTempTargetDir.'/dictionaries';
@@ -654,7 +594,7 @@ EOF;
$this->sMainPHPCode .= $aSnippet['content']."\n";
}
}
// Compile the portals
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
@@ -666,7 +606,7 @@ EOF;
// Compile the XML parameters
$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)
@@ -700,7 +640,7 @@ EOF;
$sCurrDate = date(DATE_ISO8601);
// Autoload
$sPHPFile = $sTempTargetDir.'/autoload.php';
$sPHPFileContent =
$sPHPFileContent =
<<<EOF
<?php
//
@@ -709,7 +649,7 @@ EOF;
//
EOF
;
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
$sPHPFileContent .= implode("\n", $aDataModelFiles);
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
@@ -717,14 +657,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
@@ -756,7 +696,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(
@@ -766,7 +706,7 @@ EOF
'must_change' => 'OPT_ATT_MUSTCHANGE',
'hidden' => 'OPT_ATT_HIDDEN',
);
$aFlags = array();
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
{
@@ -778,7 +718,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;
@@ -800,7 +740,7 @@ EOF
'details' => 'LINKSET_TRACKING_DETAILS',
'all' => 'LINKSET_TRACKING_ALL',
);
static $aXmlToPHP_Others = array(
'none' => 'ATTRIBUTE_TRACKING_NONE',
'all' => 'ATTRIBUTE_TRACKING_ALL',
@@ -841,7 +781,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'");
@@ -849,10 +789,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 == '')
@@ -945,7 +885,7 @@ EOF
else
{
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
}
}
}
/**
@@ -1049,7 +989,7 @@ EOF
/**
* Adds quotes and escape characters
*/
*/
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
{
if ($bSimpleQuotes)
@@ -1084,7 +1024,7 @@ EOF
$sScalar = (string)(int)$sText;
}
break;
case 'float':
if (is_null($sText))
{
@@ -1096,7 +1036,7 @@ EOF
$sScalar = (string)(float)$sText;
}
break;
case 'bool':
if (is_null($sText))
{
@@ -1328,7 +1268,7 @@ EOF
// $oField
$sAttCode = $oField->getAttribute('id');
$sAttType = $oField->getAttribute('xsi:type');
$aDependencies = array();
$oDependencies = $oField->GetOptionalElement('dependencies');
if (!is_null($oDependencies))
@@ -1340,9 +1280,9 @@ EOF
}
}
$sDependencies = 'array('.implode(', ', $aDependencies).')';
$aParameters = array();
if ($sAttType == 'AttributeLinkedSetIndirect')
{
$aParameters['linked_class'] = $this->GetMandatoryPropString($oField, 'linked_class');
@@ -2416,7 +2356,7 @@ CSS;
* @return array
* @throws \DOMFormatException
*/
protected function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
public function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
{
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
@@ -2512,11 +2452,11 @@ CSS;
case '1':
$sSearchFormOpen = 'true';
break;
case '0':
$sSearchFormOpen = 'false';
break;
default:
$sSearchFormOpen = 'true';
}
@@ -2605,7 +2545,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');
@@ -2631,7 +2571,7 @@ CSS;
$oClasses = $oGroup->GetUniqueElement('classes');
foreach($oClasses->getElementsByTagName('class') as $oClass)
{
$sClass = $oClass->getAttribute("id");
$aClasses[] = $sClass;
@@ -2649,7 +2589,7 @@ CSS;
$aProfiles[1] = array(
'name' => 'Administrator',
'description' => 'Has the rights on everything (bypassing any control)',
);
);
$aGrants = array();
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
@@ -2681,7 +2621,7 @@ CSS;
}
$sGrant = $oAction->GetText();
$bGrant = ($sGrant == 'allow');
if ($sGroupId == '*')
{
$aGrantClasses = array('*');
@@ -2920,7 +2860,7 @@ Dict::SetLanguagesList(
$sLanguagesDump
);
EOF;
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
}
@@ -2957,7 +2897,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);
@@ -2971,7 +2911,7 @@ EOF;
}
$oParentNode = $oFileRef->parentNode;
$oParentNode->removeChild($oFileRef);
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
$oParentNode->appendChild($oTextNode);
}
@@ -3284,8 +3224,8 @@ EOF;
{
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
}
// Compile themes
// Compile themes
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
}
}
@@ -3326,11 +3266,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";
@@ -3373,7 +3313,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";

View File

@@ -0,0 +1,287 @@
<?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;
}
}

View File

@@ -0,0 +1,27 @@
<?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' => [],
],
];
}
}

View File

@@ -0,0 +1,234 @@
<?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
}
}

View File

@@ -0,0 +1,24 @@
<?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);
}
}

View File

@@ -9,6 +9,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\Field;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
use Combodo\iTop\Application\UI\Base\UIBlock;
use utils;
/**
* @since 3.0.0
@@ -45,6 +46,11 @@ class Field extends UIContentBlock
protected $sValueRaw;
/** @var string */
protected $sLabel;
/**
* @var string
* @since 3.1.0
*/
protected $sDescription = '';
/** @var string */
protected $sValueId;
@@ -354,4 +360,34 @@ class Field extends UIContentBlock
return $this;
}
/**
* @return string
* @since 3.1.0
*/
public function GetDescription(): string
{
return $this->sDescription;
}
/**
* @param string $sDescription
*
* @return $this
* @since 3.1.0
*/
public function SetDescription(string $sDescription)
{
$this->sDescription = $sDescription;
return $this;
}
/*
* @return bool
* @since 3.1.0
*/
public function HasDescription(): bool
{
return utils::IsNotNullOrEmptyString($this->GetDescription());
}
}

View File

@@ -105,12 +105,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
* @api
* @param string $sLabel
* @param string $sValueHtml
* @param string $sDescription
*
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
*/
public static function MakeLarge(string $sLabel, string $sValueHtml = '')
public static function MakeLarge(string $sLabel, string $sValueHtml = '', string $sDescription = '')
{
$oField = new Field($sLabel, new Html($sValueHtml));
$oField->SetDescription($sDescription);
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_LARGE);
return $oField;
}
@@ -119,12 +121,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
* @api
* @param string $sLabel
* @param string $sValueHtml
* @param string $sDescription
*
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
*/
public static function MakeSmall(string $sLabel, string $sValueHtml = '')
public static function MakeSmall(string $sLabel, string $sValueHtml = '', string $sDescription = '')
{
$oField = new Field($sLabel, new Html($sValueHtml));
$oField->SetDescription($sDescription);
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_SMALL);
return $oField;
}
@@ -134,14 +138,16 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
* @param string $sLabel
* @param string $sLayout
* @param string|null $sId
* @param string $sDescription
*
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
*/
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null)
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null, string $sDescription = '')
{
$oField = new Field($sLabel, null, $sId);
$oField->SetDescription($sDescription);
$oField->SetLayout($sLayout);
return $oField;
}
}
}

View File

@@ -49,6 +49,16 @@ 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
*

View File

@@ -21,13 +21,14 @@ 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 iPopupMenuExtension;
use JSPopupMenuItem;
use MetaModel;
use SeparatorPopupMenuItem;
use URLPopupMenuItem;
use iPopupMenuExtension;
use UserRights;
use utils;
@@ -56,30 +57,68 @@ class PopoverMenuFactory
->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT)
->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE);
$aUserMenuItems = [];
// Allowed portals
$aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu();
if (!empty($aAllowedPortalsItems)) {
$oMenu->AddSection('allowed_portals')
->SetItems('allowed_portals', $aAllowedPortalsItems);
}
self::AddPopoverMenuItems($aAllowedPortalsItems, $aUserMenuItems);
// User related pages
$oMenu->AddSection('user_related')
->SetItems('user_related', static::PrepareUserRelatedItemsForUserMenu());
self::AddPopoverMenuItems(static::PrepareUserRelatedItemsForUserMenu(), $aUserMenuItems);
// API: iPopupMenuExtension::MENU_USER_ACTIONS
$aAPIItems = static::PrepareAPIItemsForUserMenu($oMenu);
if (count($aAPIItems) > 0) {
$oMenu->AddSection('popup_menu_extension-menu_user_actions')
->SetItems('popup_menu_extension-menu_user_actions', $aAPIItems);
}
self::AddPopoverMenuItems($aAPIItems, $aUserMenuItems);
// Misc links
$oMenu->AddSection('misc')
->SetItems('misc', static::PrepareMiscItemsForUserMenu());
/*$oMenu->AddSection('misc')
->SetItems('misc', static::PrepareMiscItemsForUserMenu());*/
self::AddPopoverMenuItems(static::PrepareMiscItemsForUserMenu(), $aUserMenuItems);
self::SortPopoverMenuItems($aUserMenuItems);
$oMenu->AddSection('misc')
->AddItems('misc', $aUserMenuItems);
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
@@ -273,4 +312,4 @@ class PopoverMenuFactory
return $oMenu;
}
}
}

View File

@@ -19,7 +19,6 @@
namespace Combodo\iTop\Application\UI\Base\Layout\NavigationMenu;
use ApplicationContext;
use ApplicationMenu;
use appUserPreferences;
@@ -35,6 +34,7 @@ use MetaModel;
use UIExtKeyWidget;
use UserRights;
use utils;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu\NewsroomMenuFactory;
/**
* Class NavigationMenu
@@ -202,7 +202,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
}
return '';
}
/**
* @return array
*/
@@ -275,7 +275,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
*/
public function IsNewsroomEnabled(): bool
{
return MetaModel::GetConfig()->Get('newsroom_enabled');
return (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders());
}
/**
@@ -296,6 +296,14 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
}
}
/**
* @return True if the silo selection is enabled, false otherwise
* @since 3.1.0
*/
public function IsSiloSelectionEnabled() : bool {
return MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
}
/**
* @return void
* @throws \CoreException
@@ -307,6 +315,10 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
$this->bHasSiloSelected = false;
$this->sSiloLabel = null;
if (! $this->IsSiloSelectionEnabled()){
return;
}
//TODO 3.0 Use components if we have the time to build select/autocomplete components before release
// List of visible Organizations
$iCount = 0;
@@ -349,7 +361,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
$this->aSiloSelection['html'] = '<form data-role="ibo-navigation-menu--silo-selection--form" action="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php">'; //<select class="org_combo" name="c[org_id]" title="Pick an organization" onChange="this.form.submit();">';
$oPage = new \CaptureWebPage();
$oWidget = new UIExtKeyWidget('Organization', 'org_id', '', true /* search mode */);
$iMaxComboLength = MetaModel::GetConfig()->Get('max_combo_length');
$this->aSiloSelection['html'] .= $oWidget->DisplaySelect($oPage, $iMaxComboLength, false, Dict::S('UI:Layout:NavigationMenu:Silo:Label'), $oSet, $iCurrentOrganization, false, 'c[org_id]', '',
@@ -382,7 +394,7 @@ $sAddClearButton
JS;
}
}
/**
* Compute if the menu is expanded or collapsed
*
@@ -502,4 +514,4 @@ JS;
$this->bShowMenusCount = $bShowMenusCount;
}
}
}

View File

@@ -48,7 +48,7 @@ class NavigationMenuFactory
{
$oNewsroomMenu = null;
if (MetaModel::GetConfig()->Get('newsroom_enabled'))
if (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders())
{
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
}

View File

@@ -23,6 +23,9 @@
{% endif %}
>
<div class="ibo-field--label">{{ oUIBlock.GetLabel()|raw }}
{% if oUIBlock.HasDescription() %}
<span class="ibo-has-description" data-tooltip-content="{{ oUIBlock.GetDescription() }}" data-tooltip-max-width="600px" ></span>
{% endif %}
{% if oUIBlock.GetLayout() == constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %}
{% if oUIBlock.GetComments() %}
<div class="ibo-field--comments">{{ oUIBlock.GetComments()|raw }}</div>

View File

@@ -0,0 +1,10 @@
<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>

View File

@@ -1,14 +1,25 @@
<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 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>

View File

@@ -1,14 +1,38 @@
$('#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();
}}],
$('#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();
}
}
});
if ($('#welcome_popup').height() > ($(window).height()-70))
{
$('#welcome_popup').height($(window).height()-70);
}

View File

@@ -0,0 +1,67 @@
<?php
namespace UI\Base\Layout;
use ApplicationContext;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
use Combodo\iTop\Application\UI\Base\Layout\NavigationMenu\NavigationMenu;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* Class NavigationMenuTest
*
* @package UI\Base\Layout
*/
class NavigationMenuTest extends ItopDataTestCase {
public function IsAllowedProvider(){
return [
'show menu' => [ true ],
'hide menu' => [ false ],
];
}
/**
* @dataProvider IsAllowedProvider
* test used to make sure backward compatibility is ensured
*/
public function testIsAllowed($bExpectedIsAllowed=true){
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', $bExpectedIsAllowed);
$oNavigationMenu = new NavigationMenu(
$this->createMock(ApplicationContext::class),
$this->createMock(PopoverMenu::class));
$isAllowed = $oNavigationMenu->IsSiloSelectionEnabled();
$this->assertEquals($bExpectedIsAllowed, $isAllowed);
}
public function testIsAllowed_BackwardCompatibility_NoVariableInConfFile(){
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', false);
$sTmpFilePath = tempnam(sys_get_temp_dir(), 'test_');
$oInitConfig = \MetaModel::GetConfig();
$oInitConfig->WriteToFile($sTmpFilePath);
//remove variable for the test
$aLines = file($sTmpFilePath);
$aRows = array();
foreach ($aLines as $key => $sLine) {
if (!preg_match('/navigation_menu.show_organization_filter/', $sLine)) {
$aRows[] = $sLine;
}
}
file_put_contents($sTmpFilePath, implode("\n", $aRows));
$oTempConfig = new \Config($sTmpFilePath);
$isAllowed = $oTempConfig->Get('navigation_menu.show_organization_filter');
$this->assertEquals(true, $isAllowed);
unlink($sTmpFilePath);
}
}

View File

View File

@@ -0,0 +1,344 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* created a dedicated test for external keys imports.
*
* Class BulkChangeExtKeyTest
*
* @package Combodo\iTop\Test\UnitTest\Core
*/
class BulkChangeExtKeyTest extends ItopDataTestCase {
const CREATE_TEST_ORG = true;
/**
* this test may delete Person objects to cover all usecases
* DO NOT CHANGE USE_TRANSACTION value to avoid any DB loss!
*/
const USE_TRANSACTION = true;
private $sUid;
protected function setUp() : void {
parent::setUp();
require_once(APPROOT.'core/bulkchange.class.inc.php');
}
private function deleteAllRacks(){
$oSearch = \DBSearch::FromOQL("SELECT Rack");
$oSet = new \DBObjectSet($oSearch);
$iCount = $oSet->Count();
if ($iCount != 0){
while ($oRack = $oSet->Fetch()){
$oRack->DBDelete();
}
}
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey){
$this->deleteAllRacks();
$this->performBulkChangeTest(
'There are no \'Rack\' objects',
"",
null,
$bIsRackReconKey
);
}
public function createRackObjects($aRackDict) {
foreach ($aRackDict as $iOrgId => $aRackNames) {
foreach ($aRackNames as $sRackName) {
$this->createObject('Rack', ['name' => $sRackName, 'description' => "${sRackName}Desc", 'org_id' => $iOrgId]);
}
}
}
private function createAnotherUserInAnotherOrg() {
$oOrg2 = $this->CreateOrganization('UnitTestOrganization2');
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Configuration Manager'), true);
$sUid = $this->GetUid();
$oUserProfile = new \URP_UserProfile();
$oUserProfile->Set('profileid', $oProfile->GetKey());
$oUserProfile->Set('reason', 'UNIT Tests');
$oSet = \DBObjectSet::FromObject($oUserProfile);
$oPerson = $this->CreatePerson('666', $oOrg2->GetKey());
$oUser = $this->createObject('UserLocal', array(
'contactid' => $oPerson->GetKey(),
'login' => $sUid,
'password' => "ABCdef$sUid@12345",
'language' => 'EN US',
'profile_list' => $oSet,
));
$oAllowedOrgList = $oUser->Get('allowed_org_list');
/** @var \URP_UserOrg $oUserOrg */
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]);
$oAllowedOrgList->AddItem($oUserOrg);
$oUser->Set('allowed_org_list', $oAllowedOrgList);
$oUser->DBWrite();
return [$oOrg2, $oUser];
}
public function ReconciliationKeyProvider(){
return [
'rack_id NOT a reconcilication key' => [ false ],
'rack_id reconcilication key' => [ true ],
];
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
\UserRights::Login($oUser->Get('login'));
$this->performBulkChangeTest(
"There are no 'Rack' objects found with your current profile",
"",
$oOrg2,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2'],
$oOrg2->GetKey() => ['RackTest3', 'RackTest4'],
]
);
\UserRights::Login($oUser->Get('login'));
$this->performBulkChangeTest(
"There are some 'Rack' objects not visible with your current profile",
"Some possible 'Rack' value(s): RackTest3, RackTest4",
$oOrg2,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
$this->performBulkChangeTest(
"No match for value 'UnexistingRack'",
"Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...",
null,
$bIsRackReconKey
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack']
]
);
$this->performBulkChangeTest(
"Invalid value for attribute",
"Ambiguous: found 2 objects",
null,
$bIsRackReconKey,
null,
null,
null,
'Found 2 matches'
);
}
/**
* @dataProvider ReconciliationKeyProvider
*/
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey){
$this->deleteAllRacks();
$this->createRackObjects(
[
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
]
);
$aCsvData = [["UnexistingRackDescription"]];
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]];
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2BAND%2B%2528%2560Rack%2560.%2560description%2560%2B%253D%2B%253Adescription%2529%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%252C%2522description%2522%253A%2522UnexistingRackDescription%2522%257D%252C%255B%255D%255D'
;
$this->performBulkChangeTest(
"No match for value 'UnexistingRack UnexistingRackDescription'",
"Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...",
null,
$bIsRackReconKey,
$aCsvData,
$aExtKeys,
$sSearchLinkUrl
);
}
private function GetUid(){
if (is_null($this->sUid)){
$this->sUid = date('dmYHis');
}
return $this->sUid;
}
/** *
* @param $aInitData
* @param $aCsvData
* @param $aAttributes
* @param $aExtKeys
* @param $aReconcilKeys
*/
public function performBulkChangeTest($sExpectedDisplayableValue, $sExpectedDescription, $oOrg, $bIsRackReconKey,
$aAdditionalCsvData=null, $aExtKeys=null, $sSearchLinkUrl=null, $sError="Object not found") {
if ($sSearchLinkUrl===null){
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%257D%252C%255B%255D%255D';
}
if (is_null($oOrg)){
$iOrgId = $this->getTestOrgId();
$sOrgName = "UnitTestOrganization";
}else{
$iOrgId = $oOrg->GetKey();
$sOrgName = $oOrg->Get('name');
}
$sUid = $this->GetUid();
$aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]];
if ($aAdditionalCsvData !== null){
foreach ($aAdditionalCsvData as $i => $aData){
foreach ($aData as $sData){
$aCsvData[$i][] = $sData;
}
}
}
$aAttributes = ["name" => 2];
if ($aExtKeys == null){
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]];
}
$aReconcilKeys = [ "name" ];
$aResult = [
0 => $sOrgName,
"org_id" => $iOrgId,
1 => "UnexistingRack",
2 => "\"$sUid\"",
"rack_id" => [
$sExpectedDisplayableValue,
$sExpectedDescription
],
"__STATUS__" => "Issue: Unexpected attribute value(s)",
"__ERRORS__" => $sError,
];
if ($bIsRackReconKey){
$aReconcilKeys[] = "rack_id";
$aResult[2] = $sUid;
$aResult["__STATUS__"] = "Issue: failed to reconcile";
}
CMDBSource::Query('START TRANSACTION');
//change value during the test
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
$this->debug("aCsvData:".json_encode($aCsvData[0]));
$this->debug("aReconcilKeys:". var_export($aReconcilKeys));
$oBulk = new \BulkChange(
"Server",
$aCsvData,
$aAttributes,
$aExtKeys,
$aReconcilKeys,
null,
null,
"Y-m-d H:i:s", // date format
true // localize
);
$this->debug("BulkChange:");
$oChange = \CMDBObject::GetCurrentChange();
$this->debug("GetCurrentChange:");
$aRes = $oBulk->Process($oChange);
$this->debug("Process:");
static::assertNotNull($aRes);
$this->debug("assertNotNull:");
var_dump($aRes);
foreach ($aRes as $aRow) {
if (array_key_exists('__STATUS__', $aRow)) {
$sStatus = $aRow['__STATUS__'];
$this->debug("sStatus:".$sStatus->GetDescription());
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
foreach ($aRow as $i => $oCell) {
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
$this->debug("i:".$i);
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
if (array_key_exists($i,$aResult)) {
$this->debug("aResult:".var_export($aResult[$i]));
if ($oCell instanceof \CellStatus_SearchIssue ||
$oCell instanceof \CellStatus_Ambiguous) {
$this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(),
"failure on ".get_class($oCell).' cell type');
$this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(),
"failure on ".get_class($oCell).' cell type');
$this->assertEquals($aResult[$i][1], $oCell->GetDescription(),
"failure on ".get_class($oCell).' cell type');
}
}
} else if ($i === "__ERRORS__") {
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
$this->assertEquals( $sErrors, $oCell->GetDescription());
}
}
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
}
}
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DBBackup;
use DBRestore;
use MetaModel;
use SetupUtils;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class DBBackupDataTest extends ItopDataTestCase
{
/**
* @dataProvider prepareFilesToBackupProvider
*/
public function testPrepareFilesToBackup(array $aExtraFiles, bool $bUnsafeFileException)
{
$sTmpDir = sys_get_temp_dir().'/testPrepareFilesToBackup-'.time();
$oBackup = new DBBackup(MetaModel::GetConfig());
MetaModel::GetConfig()->SetModuleSetting('itop-backup', 'extra_files', array_keys($aExtraFiles));
foreach($aExtraFiles as $sExtraFile => $bExists)
{
if ($bExists)
{
@mkdir(dirname(APPROOT.'/'.$sExtraFile), 0755, true);
file_put_contents(APPROOT.'/'.$sExtraFile, 'Hello World!');
}
}
if ($bUnsafeFileException)
{
$this->expectExceptionMessage("Backup: Aborting, resource '$sExtraFile'. Considered as UNSAFE because not inside the iTop directory.");
}
$aFiles = $this->InvokeNonPublicMethod('DBBackup', 'PrepareFilesToBackup', $oBackup, [APPROOT.'/conf/production/config-itop.php', $sTmpDir, true]);
SetupUtils::rrmdir($sTmpDir);
$aExpectedFiles = [
$sTmpDir.'/config-itop.php',
];
foreach($aExtraFiles as $sRelFile => $bExists)
{
if ($bExists)
{
$aExpectedFiles[] = $sTmpDir.'/'.$sRelFile;
}
}
sort($aFiles);
sort($aExpectedFiles);
$this->assertEquals($aFiles, $aExpectedFiles);
// Cleanup
foreach($aExtraFiles as $sExtraFile => $bExists)
{
if ($bExists)
{
unlink(APPROOT.'/'.$sExtraFile);
}
}
}
function prepareFilesToBackupProvider()
{
return [
'no_extra_file' => ['aExtraFiles' => [], false],
'one_extra_file' => ['aExtraFiles' => ['foo.txt' => true], false],
'three_extra_file_and_dir' => ['aExtraFiles' => ['foo.txt' => true, 'gabu/zomeu.xml' => true, 'meuh.html' => true], false],
'two_extra_file_but_only_one_exists' => ['aExtraFiles' => ['foo.txt' => true, 'meuh.html' => false], false],
'one_unsafe_file' => ['aExtraFiles' => ['../foo.txt' => true], true],
];
}
/**
* @dataProvider restoreListExtraFilesProvider
*/
function testRestoreListExtraFiles($aFilesToCreate, $aExpectedRelativeExtraFiles)
{
require_once(APPROOT.'/env-production/itop-backup/dbrestore.class.inc.php');
$sTmpDir = sys_get_temp_dir().'/testRestoreListExtraFiles-'.time();
foreach($aFilesToCreate as $sRelativeName)
{
$sDir = $sTmpDir.'/'.dirname($sRelativeName);
if(!is_dir($sDir))
{
mkdir($sDir, 0755, true);
}
file_put_contents($sTmpDir.'/'.$sRelativeName, 'Hello world.');
}
$aExpectedExtraFiles = [];
foreach($aExpectedRelativeExtraFiles as $sRelativeName)
{
$aExpectedExtraFiles[$sTmpDir.'/'.$sRelativeName] = APPROOT.'/'.$sRelativeName;
}
$oRestore = new DBRestore(MetaModel::GetConfig());
$aExtraFiles = $this->InvokeNonPublicMethod('DBRestore', 'ListExtraFiles', $oRestore, [$sTmpDir]);
asort($aExtraFiles);
asort($aExpectedExtraFiles);
$this->assertEquals($aExpectedExtraFiles, $aExtraFiles);
SetupUtils::rrmdir($sTmpDir);
}
function restoreListExtraFilesProvider()
{
return [
'no extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml'], 'aExpectedExtraFiles' => []],
'no extra file (2)' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php'], 'aExpectedExtraFiles' => []],
'one extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php', 'collectors/ldap/conf/params.local.xml'], 'aExpectedExtraFiles' => ['collectors/ldap/conf/params.local.xml']],
];
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use ApplicationMenu;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Config;
use MetaModel;
use MFCompiler;
use ParentMenuNodeCompiler;
use RunTimeEnvironment;
/**
* @group menu_compilation
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
* @since 3.1 N°4762
* @covers \MFCompiler::DoCompile
*/
class MFCompilerMenuTest extends ItopTestCase {
private static $aPreviousEnvMenus;
private static $aPreviousEnvMenuCount;
public function setUp(): void {
parent::setUp();
require_once APPROOT . 'setup/compiler.class.inc.php';
require_once APPROOT . 'setup/modelfactory.class.inc.php';
require_once APPROOT . 'application/utils.inc.php';
}
public function tearDown(): void {
parent::tearDown();
}
private function GetCurrentEnvDeltaXmlPath(string $sEnv) : string {
return APPROOT."data/$sEnv.delta.xml";
}
public function CompileMenusProvider(){
return [
'legacy_algo' => [ 'sEnv' => 'legacy_algo', 'bLegacyMenuCompilation' => true ],
'menu_compilation_fix' => [ 'sEnv' => 'menu_compilation_fix', 'bLegacyMenuCompilation' => false ],
];
}
/**
* @dataProvider CompileMenusProvider
*/
public function testCompileMenus($sEnv, $bLegacyMenuCompilation){
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
//copy conf from production to phpunit context
$sDirPath = dirname($sConfigFilePath);
if (! is_dir($sDirPath)){
mkdir($sDirPath);
}
$oConfig = new Config(\utils::GetConfigFilePath());
$oConfig->WriteToFile($sConfigFilePath);
$oConfig = new Config($sConfigFilePath);
if ($bLegacyMenuCompilation){
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
}
$oConfig->WriteToFile();
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
$oConfig->WriteToFile();
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
$aMenuGroups = ApplicationMenu::GetMenuGroups();
if (! is_null(static::$aPreviousEnvMenus)){
$this->assertEquals(static::$aPreviousEnvMenus, $aMenuGroups);
} else {
$this->assertNotEquals([], $aMenuGroups);
}
static::$aPreviousEnvMenus = $aMenuGroups;
$aMenuCount = ApplicationMenu::GetMenusCount();
if (! is_null(static::$aPreviousEnvMenuCount)){
$this->assertEquals(static::$aPreviousEnvMenuCount, $aMenuCount);
} else {
$this->assertNotEquals([], $aMenuCount);
}
static::$aPreviousEnvMenuCount = $aMenuCount;
}
public function CompileMenusWithDeltaProvider(){
return [
'Menus are broken with specific delta XML using LEGACY algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'broken_menus', 'bLegacyMenuCompilation' => true ],
'Menus repaired using same delta XML with NEW algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'fixed_menus', 'bLegacyMenuCompilation' => false ],
];
}
/**
* @dataProvider CompileMenusWithDeltaProvider
*/
public function testCompileMenusWithDelta($sDeltaFile, $sEnv, $bLegacyMenuCompilation){
$sProvidedDeltaPath = __DIR__.'/ressources/datamodels/'.$sDeltaFile;
if (is_file($sProvidedDeltaPath)){
$sDeltaXmlPath = $this->GetCurrentEnvDeltaXmlPath($sEnv);
copy($sProvidedDeltaPath, $sDeltaXmlPath);
}
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
//copy conf from production to phpunit context
$sDirPath = dirname($sConfigFilePath);
if (! is_dir($sDirPath)){
mkdir($sDirPath);
}
$oConfig = new Config(\utils::GetConfigFilePath());
$oConfig->WriteToFile($sConfigFilePath);
$oConfig = new Config($sConfigFilePath);
if ($bLegacyMenuCompilation){
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
}
$oConfig->WriteToFile();
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
$oConfig->WriteToFile();
if ($bLegacyMenuCompilation){
/**
* PHP Notice: Undefined index: ConfigManagement in /var/www/html/iTop/env-broken_menus/itop-structure/model.itop-structure.php on line 925
*/
error_reporting(E_ALL & ~E_NOTICE);
$this->expectErrorMessage("Call to a member function GetIndex() on null");
}
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
$this->assertNotEquals([], ApplicationMenu::GetMenuGroups());
$this->assertNotEquals([], ApplicationMenu::GetMenusCount());
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0">
<menus>
<menu id="Contact" xsi:type="DashboardMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
<parent _delta="redefine">ConfigManagementOverview</parent>
</menu>
<menu id="Location" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="delete">
<parent _delta="redefine">ConfigManagementOverview</parent>
</menu>
<menu id="Document" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
<parent _delta="redefine">ConfigManagementOverview</parent>
</menu>
</menus>
</itop_design>

View File

@@ -0,0 +1,198 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Webservices;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class ImportTest extends ItopDataTestCase {
const USE_TRANSACTION = false;
private $sUrl;
private $sUid;
private $sLogin;
private $sPassword = "abcDEF12345##";
private $sTmpFile = "";
private $oOrg;
protected function tearDown() : void{
parent::tearDown();
if (!empty($this->sTmpFile) && is_file($this->sTmpFile)){
unlink($this->sTmpFile);
}
}
protected function setUp() : void{
parent::setUp();
$this->sTmpFile = tempnam(sys_get_temp_dir(), 'import_csv_');
require_once(APPROOT.'application/startup.inc.php');
$this->sUid = date('dmYHis');
$this->sLogin = "import-" .$this->sUid;
$this->oOrg = $this->CreateOrganization($this->sUid);
$sConfigFile = \utils::GetConfig()->GetLoadedFile();
@chmod($sConfigFile, 0770);
$this->sUrl = \MetaModel::GetConfig()->Get('app_root_url');
@chmod($sConfigFile, 0444); // Read-only
$oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true);
$oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
if (is_object($oRestProfile) && is_object($oAdminProfile))
{
$oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword);
$this->AddProfileToUser($oUser, $oAdminProfile->GetKey());
} else {
throw new \Exception("setup failed. test cannot work as usual");
}
}
public function ImportOkProvider(){
return [
'with reconciliation key' => [ "sReconciliationKeys" => "name,first_name,org_id->name" ],
'without reconciliation key' => [ "sReconciliationKeys" => null ],
];
}
/**
* @dataProvider ImportOkProvider
*/
public function testImportOk($sReconciliationKeys){
$sFirstName = "firstname_UID";
$sLastName = "lastname_UID";
$sEmail = "email_UID@toto.fr";
$this->performImportTesting(
'"first_name","name", "email", "org_id->name"',
sprintf('"%s", "%s", "%s", UID', $sFirstName, $sLastName, $sEmail),
sprintf('ORGID;"%s";"%s";"%s"', $sFirstName, $sLastName, $sEmail),
$sReconciliationKeys,
0,
1
);
}
public function ImportFailProvider(){
return [
'without reconciliation key' => [
"sReconciliationKeys" => null,
"sExpectedLastLineNeedle" => 'Issue: Unexpected attribute value(s);n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
],
'with reconciliation key' => [
"sReconciliationKeys" => "name,first_name,org_id->name",
"sExpectedLastLineNeedle" => 'Issue: failed to reconcile;n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
],
];
}
/**
* @dataProvider ImportFailProvider
*/
public function testImportFail_ExternalKey($sReconciliationKeys, $sExpectedLastLineNeedle){
$sFirstName = "firstname_UID";
$sLastName = "lastname_UID";
$sEmail = "email_UID@toto.fr";
$this->performImportTesting(
'"first_name","name", "email", "org_id->name"',
sprintf('"%s", "%s", "%s", gabuzomeu', $sFirstName, $sLastName, $sEmail),
$sExpectedLastLineNeedle,
$sReconciliationKeys,
1,
0
);
}
public function testImportFail_Enum(){
$sFirstName = "firstname_UID";
$sLastName = "lastname_UID";
$sEmail = "email_UID@toto.fr";
$this->performImportTesting(
'"first_name","name", "email", "org_id->name", status',
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
sprintf(
'Issue: Unexpected attribute value(s);n/a;n/a;ORGID;"%s";"%s";"%s";\'toto\' is an invalid value. Unexpected value for attribute \'status\': Value not allowed [toto]', $sFirstName, $sLastName, $sEmail
),
null,
1,
0
);
}
public function testImportFail_Date(){
$sFirstName = "firstname_UID";
$sLastName = "lastname_UID";
$sEmail = "email_UID@toto.fr";
$this->performImportTesting(
'"first_name","name", "email", "org_id->name", obsolescence_date',
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
sprintf(
'Issue: Internal error: Exception, Wrong format for date attribute obsolescence_date, expecting "Y-m-d" and got "toto";n/a;n/a;n/a;%s;%s;%s;toto', $sFirstName, $sLastName, $sEmail
),
null,
1,
0
);
}
private function performImportTesting($sCsvHeaders, $sCsvFirstLineValues, $sExpectedLastLineNeedle, $sReconciliationKeys=null, $iExpectedIssue=1, $iExpectedCreated=0) {
$sContent = <<<CSVFILE
$sCsvHeaders
$sCsvFirstLineValues
CSVFILE;
file_put_contents($this->sTmpFile, str_replace("UID", $this->sUid, $sContent));
$aParams = [
'class' => 'Person',
'csvfile' => $this->sTmpFile,
'charset' => 'UTF-8',
'no_localize' => '1',
'output' => 'details',
];
if (null != $sReconciliationKeys){
$aParams["reconciliationkeys"] = $sReconciliationKeys;
}
$aRes = \utils::ExecITopScript('webservices/import.php', $aParams, $this->sLogin, $this->sPassword);
$aOutput = $aRes[1];
$sOutput = implode("\n", $aOutput);
$sLastline = $aOutput[sizeof($aOutput) - 1];
$iRes = $aRes[0];
$this->assertEquals(0, $iRes, $sOutput);
$this->assertContains("#Issues: $iExpectedIssue", $sOutput, $sOutput);
$this->assertContains("#Warnings: 0", $sOutput, $sOutput);
$this->assertContains("#Created: $iExpectedCreated", $sOutput, $sOutput);
$this->assertContains("#Updated: 0", $sOutput, $sOutput);
var_dump($sLastline);
if ($iExpectedCreated === 1) {
$this->assertContains("created;Person", $sLastline, $sLastline);
}
$iOrgId = $this->oOrg->GetKey();
$sLastLineNeedle = $sExpectedLastLineNeedle;
foreach (["ORGID" => $iOrgId, "UID" => $this->sUid] as $sSearch => $sReplace){
$sLastLineNeedle = str_replace($sSearch, $sReplace, $sLastLineNeedle);
}
$this->assertContains($sLastLineNeedle, $sLastline, $sLastline);
$sPattern = "/Person;(\d+);/";
if (preg_match($sPattern,$sLastline,$aMatches)){
var_dump($aMatches);
$iObjId = $aMatches[1];
$oObj = MetaModel::GetObject("Person", $iObjId);
$oObj->DBDelete();
}
//date
//ext key
}
}

View File

@@ -27,8 +27,8 @@ namespace Combodo\iTop\Test\UnitTest;
*/
use ArchivedObjectException;
use CMDBSource;
use CMDBObject;
use CMDBSource;
use Contact;
use DBObject;
use DBObjectSet;
@@ -70,6 +70,7 @@ define('TAG_ATTCODE', 'domains');
class ItopDataTestCase extends ItopTestCase
{
private $iTestOrgId;
// For cleanup
private $aCreatedObjects = array();
@@ -416,10 +417,25 @@ class ItopDataTestCase extends ItopTestCase
* @param string $sLogin
* @param int $iProfileId
*
* @return \DBObject
* @return \UserLocal
* @throws Exception
*/
protected function CreateUser($sLogin, $iProfileId, $sPassword=null, $iContactid=2)
{
$oUser = $this->CreateContactlessUser($sLogin, $iProfileId, $sPassword);
$oUser->Set('contactid', $iContactid);
$oUser->DBWrite();
return $oUser;
}
/**
* @param string $sLogin
* @param int $iProfileId
*
* @return \UserLocal
* @throws Exception
*/
protected function CreateContactlessUser($sLogin, $iProfileId, $sPassword=null)
{
if (empty($sPassword)){
$sPassword = $sLogin;
@@ -429,8 +445,8 @@ class ItopDataTestCase extends ItopTestCase
$oUserProfile->Set('profileid', $iProfileId);
$oUserProfile->Set('reason', 'UNIT Tests');
$oSet = DBObjectSet::FromObject($oUserProfile);
/** @var \UserLocal $oUser */
$oUser = $this->createObject('UserLocal', array(
'contactid' => $iContactid,
'login' => $sLogin,
'password' => $sPassword,
'language' => 'EN US',
@@ -455,8 +471,8 @@ class ItopDataTestCase extends ItopTestCase
$oUserProfile->Set('reason', 'UNIT Tests');
/** @var DBObjectSet $oSet */
$oSet = $oUser->Get('profile_list');
$oSet->AddObject($oUserProfile);
$oUser = $this->updateObject('UserLocal', $oUser->GetKey(), array(
$oSet->AddItem($oUserProfile);
$oUser = $this->updateObject(\User::class, $oUser->GetKey(), array(
'profile_list' => $oSet,
));
$this->debug("Updated {$oUser->GetName()} ({$oUser->GetKey()})");

View File

@@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Dict;
class DictionariesConsistencyTest extends ItopTestCase
{
@@ -148,4 +149,67 @@ class DictionariesConsistencyTest extends ItopTestCase
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
}
/**
* @dataProvider ImBulChanportCsvMessageStillOkProvider
* 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);
var_dump($sLabelValue);
} catch (\Exception $e){
$aFailedLabels[] = $sLabelKey;
var_dump([
'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;
}
}

View File

@@ -848,89 +848,6 @@ $oSearch->AddCondition_PointingTo($oOrgSearch, "org_id");
}
}
///////////////////////////////////////////////////////////////////////////
// Test bulk load API
///////////////////////////////////////////////////////////////////////////
class TestItopBulkLoad extends TestBizModel
{
static public function GetName()
{
return 'Itop - test BulkChange class';
}
static public function GetDescription()
{
return 'Execute a bulk change at the Core API level';
}
protected function DoExecute()
{
$sLogin = 'testbulkload_'.time();
$oParser = new CSVParser("login,contactid->name,password,profile_list
_1_$sLogin,Picasso,secret1,profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:toto;problem manager'
_2_$sLogin,Picasso,secret2,
", ',', '"');
$aData = $oParser->ToArray(1, array('_login', '_contact_name', '_password', '_profiles'));
self::DumpVariable($aData);
$oUser = new UserLocal();
$oUser->Set('login', 'patator');
$oUser->Set('password', 'patator');
//$oUser->Set('contactid', 0);
//$oUser->Set('language', $sLanguage);
$aProfiles = array(
array(
'profileid' => 10, // Service Manager
'reason' => 'service manager',
),
array(
'profileid->name' => 'Problem Manager',
'reason' => 'problem manager',
),
);
$oBulk = new BulkChange(
'UserLocal',
$aData,
// attributes
array('login' => '_login', 'password' => '_password', 'profile_list' => '_profiles'),
// ext keys
array('contactid' => array('name' => '_contact_name')),
// reconciliation
array('login'),
// Synchro - scope
"SELECT UserLocal",
// Synchro - set attribute on missing objects
array ('password' => 'terminated', 'login' => 'terminated'.time())
);
if (false)
{
$oMyChange = MetaModel::NewObject("CMDBChange");
$oMyChange->Set("date", time());
$oMyChange->Set("userinfo", "Testor");
$iChangeId = $oMyChange->DBInsert();
// echo "Created new change: $iChangeId</br>";
}
echo "<h3>Planned for loading...</h3>";
$aRes = $oBulk->Process();
self::DumpVariable($aRes);
if (false)
{
echo "<h3>Go for loading...</h3>";
$aRes = $oBulk->Process($oMyChange);
self::DumpVariable($aRes);
}
return;
}
}
///////////////////////////////////////////////////////////////////////////
// Test data load
///////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,96 @@
<?php
namespace UI\Base\Component\PopoverMenu;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuFactory;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class PopoverMenuFactoryTest extends ItopDataTestCase {
public function MakeUserMenuForNavigationMenuProvider(){
$aNotSortedMenuUIDs = [
'portal_itop_portal',
'UI_Preferences',
'UI_Help',
'UI_AboutBox'
];
return [
'no conf' => [
'aConf' => null,
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
],
'not an array conf' => [
'aConf' => "wrong conf",
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
],
'default conf' => [
'aConf' => [],
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
],
'same order in conf' => [
'aConf' => [
'portal:itop-portal',
'UI:Preferences',
'UI:Help',
'UI:AboutBox',
],
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
],
'first menus sorted and last one missing in conf' => [
'aConf' => [
"portal:itop-portal",
"UI:Preferences",
],
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
],
'some menus but not all sorted' => [
'aConf' => [
'UI:Preferences',
'UI:AboutBox',
],
'aExpectedMenuUIDs' => [
'UI_Preferences',
'UI_AboutBox',
'portal_itop_portal',
'UI_Help',
]
],
'all user menu sorted' => [
'aConf' => [
'UI:Preferences',
'UI:AboutBox',
'portal:itop-portal',
'UI:Help',
],
'aExpectedMenuUIDs' => [
'UI_Preferences',
'UI_AboutBox',
'portal_itop_portal',
'UI_Help',
]
],
];
}
/**
* @dataProvider MakeUserMenuForNavigationMenuProvider
*/
public function testMakeUserMenuForNavigationMenu($aConf, $aExpectedMenuUIDs){
if (! is_null($aConf)){
\MetaModel::GetConfig()->Set('navigation_menu.sorted_popup_user_menu_items', $aConf);
}
$aRes = PopoverMenuFactory::MakeUserMenuForNavigationMenu()->GetSections();
$this->assertTrue(array_key_exists('misc', $aRes));
$aUIDsWithDummyRandoString = array_keys($aRes['misc']['aItems']);
//replace ibo-popover-menu--item-6464cdca5ecf4214716943--UI_AboutBox by UI_AboutBox (for ex)
$aUIDs = preg_replace('/ibo-popover-menu--item-([^\-]+)--/', '', $aUIDsWithDummyRandoString);
$this->assertEquals($aExpectedMenuUIDs, $aUIDs);
}
}

View File

@@ -0,0 +1,221 @@
<?php
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class WelcomePopupTest extends ItopDataTestCase
{
/**
* @dataProvider sortOnImportanceDataProvider
*/
public function testSortOnImportance($aToSort, $aExpected)
{
$bResult = usort($aToSort, [WelcomePopupService::class, 'SortOnImportance']);
$this->assertTrue($bResult);
$this->assertEquals($aExpected, $aToSort);
}
/**
* Data provider for testSortOnImportance
* @return array[][]|string[][][][]|number[][][][]
*/
public function sortOnImportanceDataProvider()
{
return [
'empty array' => [
'to-sort' => [],
'expected' => [],
],
'3-item array' => [
'to-sort' => [
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
],
'expected' => [
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
],
],
'5-item array' => [
'to-sort' => [
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'zz1', 'title' => 'ZZ1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'zz2', 'title' => 'ZZ2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
],
'expected' => [
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'zz1', 'title' => 'ZZ1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
['id' => 'zz2', 'title' => 'ZZ2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
],
],
];
}
/**
* @dataProvider isMessageAcknowledgedDataProvider
*/
public function testIsMessageAcknowledged($sMessageId, $aCache, $bExpected)
{
$oService = new WelcomePopupService();
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetAcknowledgedMessagesCache', $oService, [$aCache]);
$this->assertEquals($bExpected, $this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageId]));
}
public function isMessageAcknowledgedDataProvider()
{
return [
'empty-cache' => [
'123', [], false,
],
'acknowledged' => [
'123', ['123'], true,
],
'non-acknowledged' => [
'456', ['123'], false,
],
];
}
/**
* @dataProvider isMessageValidDataProvider
*/
public function testIsMessageValid($aMessage, $bExpected)
{
$oService = new WelcomePopupService();
$aReasons = [];
$bResult = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageValid', $oService, [$aMessage, &$aReasons]);
if ($bResult !== $bExpected) {
print_r($aReasons);
}
$this->assertEquals($bExpected, $bResult);
if ($bResult) {
$this->assertEquals(0, count($aReasons));
} else {
$this->assertNotEquals(0, count($aReasons));
}
}
public function isMessageValidDataProvider()
{
return [
'not an array' => [
'123', false,
],
'empty array' => [
[], false,
],
'missing id' => [
['title' => 'foo', 'importance' => 0, 'html' => '<p>Hello</p>'], false,
],
'message Ok (html)' => [
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello</p>'], true,
],
'message Ok (twig)' => [
['id' => '123', 'title' => 'foo', 'importance' => 0, 'twig' => '/some/path'], true,
],
'missing html and twig' => [
['id' => '123', 'title' => 'foo', 'importance' => 0], false,
],
];
}
public function testProcessMessages()
{
// Mock a WelcomePopup message provider, with a fixed class name
$oProvider1 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider1')->getMock();
$oProvider1->expects($this->once())->method('GetMessages')->willReturn([
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello Foo</p>'],
['id' => '456', 'title' => 'bar', 'importance' => 1, 'html' => '<p>Hello Bar</p>'], // Already acknowledged will be skipped
]);
// Mock another WelcomePopup message provider, with a different class name
$oProvider2 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider2')->getMock();
$oProvider2->expects($this->once())->method('GetMessages')->willReturn([
['id' => '789', 'title' => 'Ga', 'importance' => 1, 'html' => '<p>Hello Ga</p>'],
['id' => '012', 'title' => 'Bu', 'importance' => 0, 'twig' => 'ga/bu/zo'],
['id' => '000', 'title' => 'Bu', 'importance' => 0], // Invalid, will be ignored
]);
$oService = new WelcomePopupService();
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetAcknowledgedMessagesCache', $oService, [[get_class($oProvider1).'::456']]);
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetMessagesProviders', $oService, [[$oProvider1, $oProvider2]]);
$aMessages = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'ProcessMessages', $oService, []);
$this->assertEquals(
[
['id' => '012', 'title' => 'Bu', 'importance' => 0, 'twig' => 'ga/bu/zo', 'uuid' => 'Provider2::012'],
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello Foo</p>', 'uuid' => 'Provider1::123'],
['id' => '789', 'title' => 'Ga', 'importance' => 1, 'html' => '<p>Hello Ga</p>', 'uuid' => 'Provider2::789'],
],
$aMessages
);
}
public function testAcknowledgeMessage()
{
self::CreateUser('admin-testAcknowledgeMessage', 1, '-Passw0rd!Complex-');
UserRights::Login('admin-testAcknowledgeMessage');
// Mock a WelcomePopup message provider, with a fixed class name
$oProvider1 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider1')->getMock();
$oProvider1->expects($this->exactly(2))->method('AcknowledgeMessage');
// Mock another WelcomePopup message provider, with a different class name
$oProvider2 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider2')->getMock();
$oProvider2->expects($this->exactly(1))->method('AcknowledgeMessage');
$sMessageUUID1 = get_class($oProvider1).'::0123456';
$sMessageUUID2 = get_class($oProvider1).'::456789';
$sMessageUUID3 = get_class($oProvider2).'::456789'; // Same message id but different provider / UUID
$oService = new WelcomePopupService();
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetMessagesProviders', $oService, [[$oProvider1, $oProvider2]]);
$oService->AcknowledgeMessage($sMessageUUID1);
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID1]));
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, ['-This-Message-Id-Is-Not-Ack0ledg3dged!']));
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
$oService->AcknowledgeMessage($sMessageUUID2);
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID1]));
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID2]));
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, ['-This-Message-Id-Is-Not-Ack0ledg3dged!']));
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
$oService->AcknowledgeMessage($sMessageUUID3);
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
}
/**
* @dataProvider makeStringFitInProvider
*/
public function testMakeStringFitIn($sInput, $iLimit, $sExpected)
{
$oService = new WelcomePopupService();
$sFitted = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'MakeStringFitIn', $oService, [$sInput, $iLimit]);
$this->assertTrue(mb_strlen($sFitted) <= $iLimit);
$this->assertEquals($sExpected, $sFitted);
}
public function makeStringFitInProvider()
{
return [
'Simple (no truncation)' => ['/Some/Short/EnoughName', 50, '/Some/Short/EnoughName'],
'Very long (truncated)' => ['/Some/Very/Loooooooooooooooooooooooooooong/Naaaaaaaaaaaaaaaaaaaaaaaaaame', 50, '4769a98d57a0f2e9b99483f780833faf-aaaaaaaaaaaaaaame'],
'Long More aggressive truncation' => ['/Some/Very/Loooooooooooooooooooooooooooong/Naaaaaaaaaaaaaaaaaaaaaaaaaame', 45, '4769a98d57a0f2e9b99483f780833faf-aaaaaaaaaame'],
];
}
}

View File

@@ -108,8 +108,8 @@ class CMDBObjectTest extends ItopDataTestCase
$sAdminLogin = "admin-user-".$sUid;
$sImpersonatedLogin = "impersonated-user-".$sUid;
$iAdminUserId = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
$this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
$oAdminUser = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
$oImpersonatedUser = $this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
$_SESSION = [];
\UserRights::Login($sAdminLogin);
@@ -124,28 +124,31 @@ class CMDBObjectTest extends ItopDataTestCase
if (is_null($sTrackInfo)){
CMDBObject::SetTrackInfo(null);
} else {
$sTrackInfo = $this->ReplaceByFriendlyNames($sTrackInfo, $oAdminUser, $oImpersonatedUser);
CMDBObject::SetTrackInfo($sTrackInfo);
}
$this->CreateSimpleObject();
if (is_null($sTrackInfo)){
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : no impersonation');
} else {
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : no impersonation');
}
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
'TrackInfo : admin userid');
\UserRights::Impersonate($sImpersonatedLogin);
$this->CreateSimpleObject();
if (is_null($sExpectedChangeLogWhenImpersonation)){
self::assertEquals("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", CMDBObject::GetCurrentChange()->Get('userinfo'),
$sExpectedMsg = $this->ReplaceByFriendlyNames("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", $oAdminUser, $oImpersonatedUser);
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : impersonation');
} else {
self::assertEquals($sExpectedChangeLogWhenImpersonation, CMDBObject::GetCurrentChange()->Get('userinfo'),
$sExpectedMsg = $this->ReplaceByFriendlyNames($sExpectedChangeLogWhenImpersonation, $oAdminUser, $oImpersonatedUser);
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : impersonation');
}
@@ -155,13 +158,13 @@ class CMDBObjectTest extends ItopDataTestCase
\UserRights::Deimpersonate();
$this->CreateSimpleObject();
if (is_null($sTrackInfo)){
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : no impersonation');
} else {
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
'TrackInfo : no impersonation');
}
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
'TrackInfo : admin userid');
// restore initial conditions
@@ -169,6 +172,12 @@ class CMDBObjectTest extends ItopDataTestCase
CMDBObject::SetTrackInfo($sInitialTrackInfo);
}
private function ReplaceByFriendlyNames($sMessage, $oAdminUser, $oImpersonatedUser) : string {
$sNewMessage = str_replace('AdminSurName AdminName', $oAdminUser->GetFriendlyName(), $sMessage);
$sNewMessage = str_replace('ImpersonatedSurName ImpersonatedName', $oImpersonatedUser->GetFriendlyName(), $sNewMessage);
return $sNewMessage;
}
private function CreateSimpleObject(){
/** @var \DocumentWeb $oTestObject */
$oTestObject = MetaModel::NewObject('DocumentWeb');
@@ -178,7 +187,7 @@ class CMDBObjectTest extends ItopDataTestCase
$oTestObject->DBWrite();
}
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): int {
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): \UserLocal {
/** @var \Person $oPerson */
$oPerson = $this->createObject('Person', array(
'name' => $sName,
@@ -187,8 +196,9 @@ class CMDBObjectTest extends ItopDataTestCase
));
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => $sProfileName), true);
/** @var \UserLocal $oUser */
$oUser = $this->CreateUser($sLogin, $oProfile->GetKey(), "1234567Azert@", $oPerson->GetKey());
return $oUser->GetKey();
return $oUser;
}
}

View File

@@ -232,7 +232,7 @@ if (utils::IsModeCLI())
{
// Next steps:
// specific arguments: 'csvfile'
//
//
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
$sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data');
@@ -273,7 +273,7 @@ try
//
// Read parameters
//
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
$sSep = ReadParam($oP, 'separator', 'raw_data');
$sQualifier = ReadParam($oP, 'qualifier', 'raw_data');
$sCharSet = ReadParam($oP, 'charset', 'raw_data');
@@ -326,7 +326,7 @@ try
{
$sDateFormat = null;
}
if ($sCharSet == '')
{
$sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset');
@@ -444,7 +444,7 @@ try
{
$sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
}
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
// Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute
$aRawFieldList = $oCSVParser->ListFields();
@@ -466,7 +466,7 @@ try
// Remove any trailing "star" character before the arrow (->)
// A star character at the end can be used to indicate a mandatory field
$sFieldName = $aMatches[1].'->'.$aMatches[2];
}
}
if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames))
{
$aColumns = $aKnownColumnNames[strtolower($sFieldName)];
@@ -488,7 +488,7 @@ try
throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames)));
}
}
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
$aAttList = array();
$aExtKeys = array();
@@ -723,7 +723,7 @@ try
}
CMDBObject::SetTrackInfo($sMoreInfo);
CMDBObject::SetTrackOrigin('csv-import.php');
$oMyChange = CMDBObject::GetCurrentChange();
}
@@ -758,7 +758,7 @@ try
break;
case 'RowStatus_Issue':
$iCountErrors++;
break;
break;
}
if ($bWritten)
@@ -837,7 +837,7 @@ try
$aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel);
}
}
$aResultDisp = array(); // to be displayed
foreach($aRes as $iRow => $aRowData)
{
@@ -864,14 +864,16 @@ try
foreach($aRowData as $key => $value)
{
$sKey = (string) $key;
if ($sKey == '__STATUS__') continue;
//__ERRORS__ used by tests only
if ($sKey == '__ERRORS__') continue;
if ($sKey == 'finalclass') continue;
if ($sKey == 'id') continue;
if (is_object($value))
{
$aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription();
$aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription();
}
else
{
@@ -885,15 +887,15 @@ try
}
catch(BulkLoadException $e)
{
$oP->add_comment($e->getMessage());
$oP->add_comment($e->getMessage());
}
catch(SecurityException $e)
{
$oP->add_comment($e->getMessage());
$oP->add_comment($e->getMessage());
}
catch(Exception $e)
{
$oP->add_comment((string)$e);
$oP->add_comment((string)$e);
}
$oP->output();