Compare commits

...

27 Commits

Author SHA1 Message Date
Benjamin DALSASS
43121a5a4b N°9379 - PHP unserialze function - security hardening
- typo
2026-04-09 16:34:35 +02:00
Benjamin DALSASS
ff2f10e5b6 N°9379 - PHP unserialze function - security hardening
- ormcaselog index
2026-04-09 15:26:49 +02:00
Benjamin DALSASS
ddaf014898 N°9379 - PHP unserialze function - security hardening
- Ensure shortcut doen't contain php objects, otherwise delete shortcut
2026-04-09 14:59:06 +02:00
Benjamin DALSASS
a71fefa328 N°9379 - PHP unserialze function - security hardening
- Create an unserialze function encapsulation
- Ensure data table settings doesn't contain php objects, otherwise revert to default settings
2026-04-09 08:31:41 +02:00
lenaick.moreira
00735f0c54 N°9483 - The search suggestion message is not displaying correctly 2026-04-08 09:48:15 +02:00
lenaick.moreira
882390e8d6 N°9043 - Fix automatic search on direct linkset block 2026-04-08 09:48:15 +02:00
Lenaick
5d0da47f21 N°8178 - Respect "high_cardinality_classes" parameter on search operation (#870) 2026-04-08 09:47:46 +02:00
lenaick.moreira
4eadff7f3b ♻️ Refactor BODY_DATA_GUI_TYPE constants to public visibility (N°9101) 2026-04-08 09:38:40 +02:00
Molkobain
f66ce1c956 Fix cURL calls to iTop within Docker dev. environment with PHP version autodetection (#869) 2026-04-07 19:06:30 +02:00
jf-cbd
802f9f3e08 N°8576 - [RequestTemplate] Slowness during the selection of drop-down list field (#872) 2026-04-07 17:06:21 +02:00
lenaick.moreira
5c5d98bb78 Enable the retrieval of all data for event notifications in the activity panel 2026-04-01 16:12:36 +02:00
Stephen Abello
a08a9b43f3 N°8737 - Fix search criteria dropdown vertical overflow (#867)
* N°8737 - Fix search criteria dropdown vertical overflow

* Update css/backoffice/components/_search-form.scss
2026-04-01 14:57:10 +02:00
jf-cbd
abd85ff4db Merge remote-tracking branch 'origin/support/3.2' into support/3.2 2026-04-01 10:37:59 +02:00
jf-cbd
81f328b26e N°8543 - rename security parameter with new convention 2026-04-01 10:37:46 +02:00
lenaick.moreira
9a2c8f10bf Fix CliResetSessionTest by allowing empty POST fields to be defined in the curl options 2026-04-01 10:35:59 +02:00
Lenaick
3cdadf3c6e Add tests for lock acquisition functionality (#865) 2026-03-31 17:01:22 +02:00
jf-cbd
a6295f1b14 Duplicate try-catch to avoid finally statement in case of uncorrrecth auth 2026-03-31 16:39:05 +02:00
Stephen Abello
e467ca83cf N°8532 - Apply filters on all DBSearch classes (#848)
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
2026-03-31 15:41:28 +02:00
Stephen Abello
7791585387 N°9231 - Make OrmDocument apply same safety to attachments and regular documents (#860) 2026-03-30 15:25:52 +02:00
Molkobain
3406ca79de N°9361 - Update PHPDoc 2026-03-30 13:49:30 +02:00
Stephen Abello
91ad01055e N°5228 - Allow themes variable imports to be overloaded by variable entries (#858) 2026-03-27 15:46:15 +01:00
Lenaick
804cdffe42 N°8234 - Fix permission checks to conditionally allow display of unauthorized objects (#859) 2026-03-27 15:09:31 +01:00
v-dumas
5f4affc896 N°9057 - Fix tests broken due to ModuleInstallation given grant_by_profile (2) 2026-03-26 17:56:52 +01:00
v-dumas
042fee2360 N°9057 - Fix tests broken due to ModuleInstallation given grant_by_profile 2026-03-26 17:37:18 +01:00
Vincent Dumas
7f8ec25977 N°9057 - Enable SuperUser to execute collectors (#799) 2026-03-26 12:13:45 +01:00
Lenaick
41f8437c23 N°8234 - Allow display of unauthorized objects in notifications and event queries (#853)
* N°8234 - Allow display of unauthorized objects in notifications and event queries

* Refactor EventNotificationNewsroom class usage in iTopNewsroomController
2026-03-26 11:44:06 +01:00
Anne-Catherine
df8b25d4b4 N°9223 - Portal - AttributeExternalKey or AttributeEnum are not displayed after adding a link. (#802) 2026-03-26 10:34:41 +01:00
51 changed files with 1409 additions and 466 deletions

View File

@@ -810,6 +810,7 @@ HTML
foreach ($aNotificationClasses as $sNotifClass) {
$aNotifSearches[$sNotifClass] = DBObjectSearch::FromOQL("SELECT $sNotifClass AS Ev JOIN Trigger AS T ON Ev.trigger_id = T.id WHERE T.id IN (:triggers) AND Ev.object_id = :id");
$aNotifSearches[$sNotifClass]->SetInternalParams($aParams);
$aNotifSearches[$sNotifClass]->AllowAllData();
$oNotifSet = new DBObjectSet($aNotifSearches[$sNotifClass], []);
$iNotifsCount += $oNotifSet->Count();
}
@@ -823,6 +824,7 @@ HTML
'menu' => false,
'panel_title' => MetaModel::GetName($sNotifClass),
'panel_icon' => MetaModel::GetClassIcon($sNotifClass, false),
'display_unauthorized_objects' => true,
]);
}
}

View File

@@ -724,6 +724,10 @@ class DisplayBlock
}
}
if (!$this->m_oFilter->IsAllDataAllowed() && ($aExtraParams['display_unauthorized_objects'] ?? false) === true) {
$this->m_oFilter->AllowAllData();
}
$aExtraParams['query_params'] = $this->m_oFilter->GetInternalParams();
$this->m_oSet = new CMDBObjectSet($this->m_oFilter, $aOrderBy, $aQueryParams);
}
@@ -1381,7 +1385,10 @@ JS
// Check the classes that can be read (i.e authorized) by this user...
foreach ($aClasses as $sAlias => $sClassName) {
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $this->m_oSet) != UR_ALLOWED_NO) {
if (
(UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $this->m_oSet) !== UR_ALLOWED_NO)
|| ($aExtraParams['display_unauthorized_objects'] ?? false) === true
) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}

View File

@@ -1546,7 +1546,16 @@ class ShortcutMenuNode extends MenuNode
public function GetHyperlink($aExtraParams)
{
$sContext = $this->oShortcut->Get('context');
$aContext = unserialize($sContext);
try {
$aContext = utils::Unserialize($sContext, ['allowed_classes' => false]);
} catch (Exception $e) {
IssueLog::Warning("User shortcut corrupted, delete the shortcut", LogChannels::CONSOLE, [
'shortcut_name' => $this->oShortcut->GetName(),
'root_cause' => $e->getMessage(),
]);
// delete the shortcut
$this->oShortcut->DBDelete();
}
if (isset($aContext['menu'])) {
unset($aContext['menu']);
}

View File

@@ -936,11 +936,6 @@ CSS;
public static function CloneThemeParameterAndIncludeVersion($aThemeParameters, $bSetupCompilationTimestamp, $aImportsPaths)
{
$aThemeParametersVariable = [];
if (array_key_exists('variables', $aThemeParameters)) {
if (is_array($aThemeParameters['variables'])) {
$aThemeParametersVariable = array_merge([], $aThemeParameters['variables']);
}
}
if (array_key_exists('variable_imports', $aThemeParameters)) {
if (is_array($aThemeParameters['variable_imports'])) {
@@ -948,6 +943,14 @@ CSS;
}
}
// Variables defined in theme XML have the priority over variables defined in XML imports files
// They're defined after so they overwrite previous parameters
if (array_key_exists('variables', $aThemeParameters)) {
if (is_array($aThemeParameters['variables'])) {
$aThemeParametersVariable = array_merge($aThemeParametersVariable, $aThemeParameters['variables']);
}
}
$aThemeParametersVariable['$version'] = $bSetupCompilationTimestamp;
return $aThemeParametersVariable;
}

View File

@@ -228,7 +228,7 @@ JS
<<<HTML
<form id="ObjectsAddForm_{$this->sInputid}">
<div id="SearchResultsToAdd_{$this->sInputid}">
<div style="background: #fff; border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
<div style="border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
</div>
<input type="hidden" id="count_{$this->sInputid}" value="0"/>
</form>

View File

@@ -68,7 +68,7 @@ class UISearchFormForeignKeys
<<<HTML
<form id="ObjectsAddForm_{$this->m_iInputId}">
<div id="SearchResultsToAdd_{$this->m_iInputId}" style="vertical-align:top;height:100%;overflow:auto;padding:0;border:0;">
<div style="background: #fff; border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
<div style="border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
</div>
<input type="hidden" id="count_{$this->m_iInputId}" value="0"/>
</form>

View File

@@ -1455,6 +1455,12 @@ class utils
case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT:
/** @var \DBObjectSet $param */
// Check if the user has the right to read the objects of this list, otherwise do not propose any action (eg. configure this list, export, etc.)
if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_READ, $param) !== UR_ALLOWED_YES) {
break;
}
$oAppContext = new ApplicationContext();
$sContext = $oAppContext->GetForLink(true);
$sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId;
@@ -3246,4 +3252,50 @@ TXT
return $aTrace;
}
/**
* PHP unserialize encapsulation, allow throwing exception when not allowed object class is detected (for security hardening)
*
* @param mixed $data data to unserialize
* @param array $aOptions PHP @unserialise options
* @param bool $bThrowNotAllowedObjectClassException flag to throw exception
*
* @return mixed PHP @unserialise return
* @throws Exception
*/
public static function Unserialize(mixed $data, array $aOptions, bool $bThrowNotAllowedObjectClassException = true): mixed
{
$data = unserialize($data, $aOptions);
if ($bThrowNotAllowedObjectClassException) {
try {
self::AssertNoIncompleteClassDetected($data);
} catch (Exception $e) {
throw new CoreException('Unserialization failed because an incomplete class was detected.', [], '', $e);
}
}
return $data;
}
/**
* Assert that data provided doesn't contain any incomplete class.
*
* @throws Exception
*/
public static function AssertNoIncompleteClassDetected(mixed $data): void
{
if (is_object($data)) {
if ($data instanceof __PHP_Incomplete_Class) {
throw new Exception('__PHP_Incomplete_Class_Name object detected');
}
foreach (get_object_vars($data) as $property) {
self::AssertNoIncompleteClassDetected($property);
}
} elseif (is_array($data)) {
foreach ($data as $value) {
self::AssertNoIncompleteClassDetected($value);
}
}
}
}

View File

@@ -234,10 +234,11 @@ abstract class Action extends cmdbAbstractObject
}
$oActionFilter = DBObjectSearch::FromOQL($sActionQueryOql, $aActionQueryParams);
$oActionFilter->AllowAllData();
$oSet = new DBObjectSet($oActionFilter, ['date' => false]);
$sPanelTitle = Dict::Format('Action:last_executions_tab_panel_title', $sActionQueryLimit);
$oExecutionsListBlock = DataTableUIBlockFactory::MakeForResult($oPage, 'action_executions_list', $oSet, ['panel_title' => $sPanelTitle]);
$oExecutionsListBlock = DataTableUIBlockFactory::MakeForResult($oPage, 'action_executions_list', $oSet, ['panel_title' => $sPanelTitle, 'display_unauthorized_objects' => true]);
$oPage->AddUiBlock($oExecutionsListBlock);
}

View File

@@ -4829,7 +4829,7 @@ class AttributeCaseLog extends AttributeLongText
}
if (strlen($sIndex) > 0) {
$aIndex = unserialize($sIndex);
$aIndex = utils::Unserialize($sIndex, ['allowed_classes' => false], false);
$value = new ormCaseLog($sLog, $aIndex);
} else {
$value = new ormCaseLog($sLog);

View File

@@ -1730,6 +1730,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.disable_joined_classes_filter' => [
'type' => 'bool',
'description' => 'If true, scope filters aren\'t applied to joined classes or union classes not directly listed in the SELECT clause.',
'default' => true,
'value' => true,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.hide_administrators' => [
'type' => 'bool',
'description' => 'If true, non-administrator users will not be able to see the administrator accounts, the Administrator profile and the links between the administrator accounts and their profiles.',
@@ -1738,11 +1746,11 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.force_login_when_no_delegated_authentication_endpoints_list' => [
'security.disable_exec_forced_login_for_all_enpoints' => [
'type' => 'bool',
'description' => 'If true, when no execution policy is defined, the user will be forced to log in (instead of being automatically logged in with the default profile)',
'default' => false,
'value' => false,
'description' => 'If true, when no delegated authentication module is defined, no login will be forced on modules exec endpoints',
'default' => true,
'value' => true,
'source_of_value' => '',
'show_in_conf_sample' => false,
],

View File

@@ -409,7 +409,7 @@
</php_parent>
<parent>cmdbAbstractObject</parent>
<properties>
<category>core/cmdb,view_in_gui</category>
<category>core/cmdb,grant_by_profile,silo</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>priv_event_newsroom</db_table>
@@ -888,7 +888,7 @@
<!-- Generated by toolkit/export-class-to-meta.php -->
<parent>Event</parent>
<properties>
<category>core/cmdb,view_in_gui</category>
<category>core/cmdb,grant_by_profile,silo</category>
</properties>
<fields>
<field id="message" xsi:type="AttributeText"/>

View File

@@ -1925,4 +1925,37 @@ class DBObjectSearch extends DBSearch
{
return $this->GetCriteria()->ListParameters();
}
/**
* @inheritDoc
* @return DBObjectSearch
*/
protected function ApplyDataFilters(): DBObjectSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;
}
$oSearch = $this;
$aClassesToFilter = $this->GetSelectedClasses();
// Opt-in for joined classes filtering, otherwise only filter the selected class(es)
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === false) {
$aClassesToFilter = $this->GetJoinedClasses();
}
// Apply filter (this is similar to the one in DBSearch but the factorization could make it less readable)
foreach ($aClassesToFilter as $sClassAlias => $sClass) {
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
if ($oVisibleObjects === false) {
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
}
if (is_object($oVisibleObjects)) {
$oVisibleObjects->AllowAllData();
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
$oSearch->SetDataFiltered();
}
}
return $oSearch;
}
}

View File

@@ -1122,21 +1122,7 @@ abstract class DBSearch
*/
protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null)
{
$oSearch = $this;
if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered()) {
foreach ($this->GetSelectedClasses() as $sClassAlias => $sClass) {
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
if ($oVisibleObjects === false) {
// Make sure this is a valid search object, saying NO for all
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
}
if (is_object($oVisibleObjects)) {
$oVisibleObjects->AllowAllData();
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
$oSearch->SetDataFiltered();
}
}
}
$oSearch = $this->ApplyDataFilters();
if (is_array($aGroupByExpr)) {
foreach ($aGroupByExpr as $sAlias => $oGroupByExp) {
@@ -1608,4 +1594,33 @@ abstract class DBSearch
* @return array{\VariableExpression}
*/
abstract public function GetExpectedArguments(): array;
/**
* Apply data filters to the search, if needed
*
* @return DBSearch
* @throws CoreException
*/
protected function ApplyDataFilters(): DBSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;
}
$oSearch = $this;
$aClassesToFilter = $this->GetSelectedClasses();
foreach ($aClassesToFilter as $sClassAlias => $sClass) {
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
if ($oVisibleObjects === false) {
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
}
if (is_object($oVisibleObjects)) {
$oVisibleObjects->AllowAllData();
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
$oSearch->SetDataFiltered();
}
}
return $oSearch;
}
}

View File

@@ -673,4 +673,30 @@ class DBUnionSearch extends DBSearch
return $aVariableCriteria;
}
/**
* @inheritDoc
* @return DBUnionSearch
*/
protected function ApplyDataFilters(): DBUnionSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;
}
// Opt-in for joined classes filtering, otherwise fallback on DBSearch filtering
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === true) {
return parent::ApplyDataFilters();
}
// Apply filters per sub-search
$aFilteredSearches = [];
foreach ($this->GetSearches() as $oSubSearch) {
// Recursively call ApplyDataFilters on sub-searches
$aFilteredSearches[] = $oSubSearch->ApplyDataFilters();
}
$oSearch = new DBUnionSearch($aFilteredSearches);
return $oSearch;
}
}

View File

@@ -26,7 +26,7 @@ class Event extends DBObject implements iDisplay
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -120,7 +120,7 @@ class EventNotification extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -154,7 +154,7 @@ class EventNotificationEmail extends EventNotification
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -190,7 +190,7 @@ class EventIssue extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -284,7 +284,7 @@ class EventWebService extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -319,7 +319,7 @@ class EventRestService extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -354,7 +354,7 @@ class EventLoginUsage extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",
@@ -392,7 +392,7 @@ class EventOnObject extends Event
{
$aParams =
[
"category" => "core/cmdb,view_in_gui",
"category" => "core/cmdb,grant_by_profile,silo",
"key_type" => "autoincrement",
"name_attcode" => "",
"state_attcode" => "",

View File

@@ -350,15 +350,18 @@ class ormDocument
if (!is_object($oObj)) {
// If access to the document is not granted, check if the access to the host object is allowed
$oObj = MetaModel::GetObject($sClass, $id, false, true);
$bHasHostRights = false;
if ($oObj instanceof Attachment) {
$sItemClass = $oObj->Get('item_class');
$sItemId = $oObj->Get('item_id');
$oHost = MetaModel::GetObject($sItemClass, $sItemId, false, false);
if (!is_object($oHost)) {
$oObj = null;
if (is_object($oHost)) {
$bHasHostRights = true;
}
}
if (!is_object($oObj)) {
// We could neither read the object nor get a host object matching our rights
if ($bHasHostRights !== true) {
throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it");
}
}

View File

@@ -21,6 +21,8 @@ $ibo-search-form-panel--more-criteria--color: $ibo-color-blue-grey-800 !default;
$ibo-search-form-panel--more-criteria--background-color: $ibo-color-white-100 !default;
$ibo-search-form-panel--more-criteria--icon--color: $ibo-color-primary-600 !default;
$ibo-search-form-panel--more-criteria--border-color: $ibo-search-form-panel--criteria--border-color !default;
// calc is redundant but avoid SCSS min() from being used instead of CSS min()
$ibo-search-form-panel--criteria--max-height: calc(min(#{$ibo-size-750}, 50vh)) !default;
$ibo-search-form-panel--items--hover--color: $ibo-color-grey-200 !default;
@@ -278,9 +280,10 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
}
.sfc_form_group {
display: block;
margin-top: -1px;
z-index: -1;
display: flex;
flex-direction: column;
margin-top: -1px;
z-index: -1;
}
}
@@ -346,11 +349,15 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
display: none;
max-width: 450px;
width: max-content;
max-height: 520px;
max-height: $ibo-search-form-panel--criteria--max-height;
overflow-x: auto;
overflow-y: hidden;
.sfc_fg_operators {
display: flex;
flex-direction: column;
overflow: auto;
min-height: 0;
font-size: 12px;
.sfc_fg_operator {
@@ -387,6 +394,9 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
}
.sfc_opc_multichoices {
display: flex;
flex-direction: column;
height: 100%;
label > input {
vertical-align: text-top;
margin-left: $ibo-spacing-0;
@@ -398,7 +408,6 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
}
.sfc_opc_mc_items_wrapper {
max-height: 415px; /* Must be less than .sfc_form_group:max-height - .sfc_opc_mc_toggler:height - .sfc_opc_mc_filter:height */
overflow-y: auto;
margin: $ibo-spacing-0 -8px; /* Compensate .sfc_opc_multichoices side padding so the hover style can take the full with */
@@ -560,8 +569,14 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
&.search_form_criteria_enum {
.sfc_form_group {
.sfc_fg_operator_in {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
> label {
display: inline-block;
display: flex;
height: 100%;
min-height: 0;
width: 100%;
line-height: initial;
white-space: nowrap;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -68,47 +68,47 @@ $ibo-color-information-900: #0f172a !default;
$ibo-color-information-950: #020617 !default;
$ibo-lifecycle-new-state-primary-color: $ibo-color-information-600;
$ibo-lifecycle-new-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-neutral-state-primary-color: $ibo-color-information-600;
$ibo-lifecycle-neutral-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-waiting-state-primary-color: $ibo-color-yellow-700;
$ibo-lifecycle-waiting-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-success-state-primary-color: $ibo-color-blue-700;
$ibo-lifecycle-success-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-failure-state-primary-color: $ibo-color-orange-800;
$ibo-lifecycle-failure-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-frozen-state-primary-color: $ibo-color-information-200;
$ibo-lifecycle-frozen-state-secondary-color: $ibo-color-information-700;
$ibo-lifecycle-new-state-primary-color: $ibo-color-information-600 !default;
$ibo-lifecycle-new-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-neutral-state-primary-color: $ibo-color-information-600 !default;
$ibo-lifecycle-neutral-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-waiting-state-primary-color: $ibo-color-yellow-700 !default;
$ibo-lifecycle-waiting-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-success-state-primary-color: $ibo-color-blue-700 !default;
$ibo-lifecycle-success-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-failure-state-primary-color: $ibo-color-orange-800 !default;
$ibo-lifecycle-failure-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-frozen-state-primary-color: $ibo-color-information-200 !default;
$ibo-lifecycle-frozen-state-secondary-color: $ibo-color-information-700 !default;
$ibo-lifecycle-active-state-primary-color: $ibo-color-blue-700;
$ibo-lifecycle-active-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-inactive-state-primary-color: $ibo-color-yellow-700;
$ibo-lifecycle-inactive-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-active-state-primary-color: $ibo-color-blue-700 !default;
$ibo-lifecycle-active-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-inactive-state-primary-color: $ibo-color-yellow-700 !default;
$ibo-lifecycle-inactive-state-secondary-color: $ibo-color-white-100 !default;
$ibo-caselog-highlight-color-1: $ibo-color-blue-700;
$ibo-caselog-highlight-color-2: $ibo-color-yellow-700;
$ibo-caselog-highlight-color-3: $ibo-color-information-600;
$ibo-caselog-highlight-color-4: $ibo-color-yellow-500;
$ibo-caselog-highlight-color-5: $ibo-color-blue-500;
$ibo-caselog-highlight-color-6: $ibo-color-yellow-300;
$ibo-caselog-highlight-color-7: $ibo-color-blue-300;
$ibo-caselog-highlight-color-1: $ibo-color-blue-700 !default;
$ibo-caselog-highlight-color-2: $ibo-color-yellow-700 !default;
$ibo-caselog-highlight-color-3: $ibo-color-information-600 !default;
$ibo-caselog-highlight-color-4: $ibo-color-yellow-500 !default;
$ibo-caselog-highlight-color-5: $ibo-color-blue-500 !default;
$ibo-caselog-highlight-color-6: $ibo-color-yellow-300 !default;
$ibo-caselog-highlight-color-7: $ibo-color-blue-300 !default;
$ibo-input-wrapper--is-error--border-color: $ibo-color-warning-700;
$ibo-field-validation: $ibo-color-warning-800;
$ibo-input-wrapper--is-error--border-color: $ibo-color-warning-700 !default;
$ibo-field-validation: $ibo-color-warning-800 !default;
$ibo-navigation-menu--visual-hint--background-color: $ibo-color-blue-400;
$ibo-navigation-menu--visual-hint--background-color: $ibo-color-blue-400 !default;
$ibo-wizard-container--background-color: $ibo-color-information-200;
$ibo-wizard-container--border-color: $ibo-color-information-600;
$ibo-wizard-container--background-color: $ibo-color-information-200 !default;
$ibo-wizard-container--border-color: $ibo-color-information-600 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--background-color: $ibo-color-white-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--border: solid 2px $ibo-color-grey-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--background-color: $ibo-color-danger-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--border: solid 2px $ibo-color-danger-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--background-color: $ibo-color-warning-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--border: solid 2px $ibo-color-warning-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--background-color: $ibo-color-success-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--border: solid 2px $ibo-color-success-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--background-color: $ibo-color-white-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--border: solid 2px $ibo-color-grey-500 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--background-color: $ibo-color-danger-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--border: solid 2px $ibo-color-danger-500 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--background-color: $ibo-color-warning-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--border: solid 2px $ibo-color-warning-500 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--background-color: $ibo-color-success-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--border: solid 2px $ibo-color-success-500 !default;
$ibo-notifications--view-all--item--unread--highlight--background-color: $ibo-color-blue-600;
$ibo-notifications--view-all--item--unread--highlight--background-color: $ibo-color-blue-600 !default;

View File

@@ -32,47 +32,47 @@ $ibo-color-information-900: #0f172a !default;
$ibo-color-information-950: #020617 !default;
$ibo-lifecycle-new-state-primary-color: $ibo-color-information-600;
$ibo-lifecycle-new-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-neutral-state-primary-color: $ibo-color-information-600;
$ibo-lifecycle-neutral-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-waiting-state-primary-color: $ibo-color-red-200;
$ibo-lifecycle-waiting-state-secondary-color: $ibo-color-red-800;
$ibo-lifecycle-success-state-primary-color: $ibo-color-blue-700;
$ibo-lifecycle-success-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-failure-state-primary-color: $ibo-color-red-800;
$ibo-lifecycle-failure-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-frozen-state-primary-color: $ibo-color-information-200;
$ibo-lifecycle-frozen-state-secondary-color: $ibo-color-information-700;
$ibo-lifecycle-new-state-primary-color: $ibo-color-information-600 !default;
$ibo-lifecycle-new-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-neutral-state-primary-color: $ibo-color-information-600 !default;
$ibo-lifecycle-neutral-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-waiting-state-primary-color: $ibo-color-red-200 !default;
$ibo-lifecycle-waiting-state-secondary-color: $ibo-color-red-800 !default;
$ibo-lifecycle-success-state-primary-color: $ibo-color-blue-700 !default;
$ibo-lifecycle-success-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-failure-state-primary-color: $ibo-color-red-800 !default;
$ibo-lifecycle-failure-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-frozen-state-primary-color: $ibo-color-information-200 !default;
$ibo-lifecycle-frozen-state-secondary-color: $ibo-color-information-700 !default;
$ibo-lifecycle-active-state-primary-color: $ibo-color-blue-700;
$ibo-lifecycle-active-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-inactive-state-primary-color: $ibo-color-red-700;
$ibo-lifecycle-inactive-state-secondary-color: $ibo-color-white-100;
$ibo-lifecycle-active-state-primary-color: $ibo-color-blue-700 !default;
$ibo-lifecycle-active-state-secondary-color: $ibo-color-white-100 !default;
$ibo-lifecycle-inactive-state-primary-color: $ibo-color-red-700 !default;
$ibo-lifecycle-inactive-state-secondary-color: $ibo-color-white-100 !default;
$ibo-caselog-highlight-color-1: $ibo-color-blue-700;
$ibo-caselog-highlight-color-2: $ibo-color-red-700;
$ibo-caselog-highlight-color-3: $ibo-color-information-600;
$ibo-caselog-highlight-color-4: $ibo-color-red-500;
$ibo-caselog-highlight-color-5: $ibo-color-blue-500;
$ibo-caselog-highlight-color-6: $ibo-color-red-300;
$ibo-caselog-highlight-color-7: $ibo-color-blue-300;
$ibo-caselog-highlight-color-1: $ibo-color-blue-700 !default;
$ibo-caselog-highlight-color-2: $ibo-color-red-700 !default;
$ibo-caselog-highlight-color-3: $ibo-color-information-600 !default;
$ibo-caselog-highlight-color-4: $ibo-color-red-500 !default;
$ibo-caselog-highlight-color-5: $ibo-color-blue-500 !default;
$ibo-caselog-highlight-color-6: $ibo-color-red-300 !default;
$ibo-caselog-highlight-color-7: $ibo-color-blue-300 !default;
$ibo-input-wrapper--is-error--border-color: $ibo-color-pink-700;
$ibo-field-validation: $ibo-color-pink-800;
$ibo-input-wrapper--is-error--border-color: $ibo-color-pink-700 !default;
$ibo-field-validation: $ibo-color-pink-800 !default;
$ibo-navigation-menu--visual-hint--background-color: $ibo-color-pink-600;
$ibo-navigation-menu--visual-hint--background-color: $ibo-color-pink-600 !default;
$ibo-wizard-container--background-color: $ibo-color-information-200;
$ibo-wizard-container--border-color: $ibo-color-information-600;
$ibo-wizard-container--background-color: $ibo-color-information-200 !default;
$ibo-wizard-container--border-color: $ibo-color-information-600 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--background-color: $ibo-color-white-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--border: solid 2px $ibo-color-grey-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--background-color: $ibo-color-pink-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--border: solid 2px $ibo-color-pink-600;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--background-color: $ibo-color-warning-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--border: solid 2px $ibo-color-warning-400;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--background-color: $ibo-color-success-100;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--border: solid 2px $ibo-color-success-500;
$ibo-navigation-menu--notifications--item--new-message-indicator--background-color: $ibo-color-white-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--border: solid 2px $ibo-color-grey-500 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--background-color: $ibo-color-pink-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-1--border: solid 2px $ibo-color-pink-600 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--background-color: $ibo-color-warning-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-2--border: solid 2px $ibo-color-warning-400 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--background-color: $ibo-color-success-100 !default;
$ibo-navigation-menu--notifications--item--new-message-indicator--is-priority-3--border: solid 2px $ibo-color-success-500 !default;
$ibo-notifications--view-all--item--unread--highlight--background-color: $ibo-color-pink-500;
$ibo-notifications--view-all--item--unread--highlight--background-color: $ibo-color-pink-500 !default;

View File

@@ -279,11 +279,24 @@ try {
$oRuntimeEnv = new RunTimeEnvironment('production', true);
try {
SetupLog::Info('Move to production starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(APPROOT.'data/hub/compile_authent') || $sAuthent !== file_get_contents(APPROOT.'data/hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
} catch (Exception $e) {
if (file_exists(APPROOT.'data/hub/compile_authent')) {
unlink(APPROOT.'data/hub/compile_authent');
}
// Note: at this point, the dictionnary is not necessarily loaded
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
ReportError($e->getMessage(), $e->getCode());
break;
}
try {
SetupLog::Info('Move to production starts...');
unlink(APPROOT.'data/hub/compile_authent');
// Load the "production" config file to clone & update it
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);

View File

@@ -1386,19 +1386,27 @@ class ObjectController extends BrickController
if ($oField instanceof DateTimeField) {
$oField->SetDateTimePickerWidgetParent($sDateTimePickerWidgetParent);
}
$sFieldRendererClass = BsLinkedSetFieldRenderer::GetFieldRendererClass($oField);
// View data
$sValue = $oAttDef->GetAsHTML($oNewLink->Get($sAttCode));
$aObjectData['attributes']['lnk__'.$sAttCode] = [
'object_class' => $sLinkClass,
'object_id' => $oNewLink->GetKey(),
'prefix' => 'lnk__',
'attribute_code' => $sAttCode,
'attribute_type' => get_class($oAttDef),
'value_html' => $sValue,
];
// If the field has a renderer we adjust view data
$sFieldRendererClass = BsLinkedSetFieldRenderer::GetFieldRendererClass($oField);
if ($sFieldRendererClass !== null) {
$oFieldRenderer = new $sFieldRendererClass($oField);
$oFieldOutput = $oFieldRenderer->Render();
$sValue = $oFieldOutput->GetHtml();
$aObjectData['attributes']['lnk__'.$sAttCode]['value_html'] = $oFieldOutput->GetHtml();
$aObjectData['attributes']['lnk__'.$sAttCode]['css_inline'] = $oFieldOutput->GetCss();
$aObjectData['attributes']['lnk__'.$sAttCode]['js_inline'] = $oFieldOutput->GetJs();
}
$aObjectData['attributes']['lnk__'.$sAttCode] = [
'att_code' => $sAttCode,
'value' => $sValue,
'css_inline' => $oFieldOutput->GetCss(),
'js_inline' => $oFieldOutput->GetJs(),
];
}
$aData['items'][] = $aObjectData;

View File

@@ -186,6 +186,7 @@
<group id="AdminSysReadOnly" _delta="define">
<classes>
<class id="ItopFenceLogin"/>
<class id="ModuleInstallation"/>
</classes>
</group>
<group id="AdminSys" _delta="define">
@@ -195,6 +196,11 @@
<class id="RessourceHybridAuthMenu"/>
</classes>
</group>
<group id="Event" _delta="define">
<classes>
<class id="Event"/>
</classes>
</group>
</groups>
<profiles>
<profile id="117" _delta="define">
@@ -290,6 +296,16 @@
<action id="stimulus:ev_close">allow</action>
</actions>
</group>
<group id="Event">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
<action id="action:write">allow</action>
<action id="action:bulk write">allow</action>
<action id="action:delete">allow</action>
<action id="action:bulk delete">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="3" _delta="define">

View File

@@ -189,11 +189,14 @@ $(function()
this.buildData.script_code = '';
this.buildData.style_code = '';
for (var i in oData.updated_fields)
for (let i in oData.updated_fields)
{
var oUpdatedField = oData.updated_fields[i];
this.options.fields_list[oUpdatedField.id] = oUpdatedField;
this._prepareField(oUpdatedField.id);
const oUpdatedField = oData.updated_fields[i];
const oPreviousField = this.options.fields_list[oUpdatedField.id];
if (!oPreviousField || JSON.stringify(oPreviousField) !== JSON.stringify(oUpdatedField)) {
this.options.fields_list[oUpdatedField.id] = oUpdatedField;
this._prepareField(oUpdatedField.id);
}
}
// Adding code to the dom

View File

@@ -193,6 +193,7 @@ return array(
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => $baseDir . '/sources/Application/Helper/SearchHelper.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\\Newsroom\\iTopNewsroomProvider' => $baseDir . '/sources/Application/Newsroom/iTopNewsroomProvider.php',

View File

@@ -548,6 +548,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/SearchHelper.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\\Newsroom\\iTopNewsroomProvider' => __DIR__ . '/../..' . '/sources/Application/Newsroom/iTopNewsroomProvider.php',

View File

@@ -5,6 +5,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\Helper\SearchHelper;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
@@ -126,72 +127,6 @@ function SetObjectBreadCrumbEntry(DBObject $oObj, WebPage $oPage)
$oPage->SetBreadCrumbEntry("ui-details-$sClass-".$oObj->GetKey(), $oObj->Get('friendlyname'), MetaModel::GetName($sClass).': '.$oObj->Get('friendlyname'), '', $sIcon, $sIconType);
}
/**
* Displays the result of a search request
* @param $oP WebPage Web page for the output
* @param $oFilter DBSearch The search of objects to display
* @param $bSearchForm boolean Whether or not to display the search form at the top the page
* @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
* @param $sFormat string The format to use for the output: csv or html
* @param $bDoSearch bool True to display the search results below the search form
* @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course)
* @throws \CoreException
* @throws \DictExceptionMissingString
*/
function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = true, $aParams = [])
{
//search block
$oBlockForm = null;
if ($bSearchForm) {
$aParams['open'] = $bSearchFormOpen;
if (false === isset($aParams['table_id'])) {
$aParams['table_id'] = 'result_1';
}
if (!empty($sBaseClass)) {
$aParams['baseClass'] = $sBaseClass;
}
$oBlockForm = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams);
if (!$bDoSearch) {
$oBlockForm->Display($oP, 0);
}
}
if ($bDoSearch) {
if (strtolower($sFormat) == 'csv') {
$oBlock = new DisplayBlock($oFilter, 'csv', false);
// Adjust the size of the Textarea containing the CSV to fit almost all the remaining space
$oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block
} else {
$oBlock = new DisplayBlock($oFilter, 'list', false);
// Breadcrumb
//$iCount = $oBlock->GetDisplayedCount();
$sPageId = "ui-search-".$oFilter->GetClass();
$sLabel = MetaModel::GetName($oFilter->GetClass());
$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
}
if ($bSearchForm) {
//add search block
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
if ($sTableId == '') {
$sTableId = 'result_1';
}
$aExtraParams['table_id'] = $sTableId;
$aExtraParams['submit_on_load'] = false;
$oUIBlockForm = $oBlockForm->GetDisplay($oP, 'search_1', $aExtraParams);
//add result block
$oUIBlock = $oBlock->GetDisplay($oP, $sTableId);
$oUIBlock->AddCSSClasses(['display_block', 'sf_results_area']);
$oUIBlock->AddDataAttribute('target', 'search_results');
//$oUIBlockForm->AddSubBlock($oUIBlock);
$oP->AddUiBlock($oUIBlockForm);
$oUIBlockForm->AddSubBlock($oUIBlock);
} else {
$oBlock->Display($oP, 1);
}
}
}
/**
* Displays a form (checkboxes) to select the objects for which to apply a given action
* Only the objects for which the action is valid can be checked. By default all valid objects are checked
@@ -460,7 +395,7 @@ try {
$sOQL = "SELECT $sOQLClass $sOQLClause";
try {
$oFilter = DBObjectSearch::FromOQL($sOQL);
DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat);
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat);
} catch (CoreException $e) {
$oFilter = new DBObjectSearch($sOQLClass);
$oSet = new DBObjectSet($oFilter);
@@ -487,7 +422,7 @@ try {
}
$oP->set_title(Dict::S('UI:SearchResultsPageTitle'));
$oFilter = new DBObjectSearch($sClass);
DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */);
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */);
break;
///////////////////////////////////////////////////////////////////////////////////////////
@@ -509,7 +444,7 @@ try {
// $sParams = utils::ReadParam('aParams', '{}', false, \utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
// $aParams = json_decode($sParams, true);
DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); //, true, true, $aParams
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); //, true, true, $aParams
break;
///////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -102,21 +102,22 @@ if ($sTargetPage === false || $sModule === 'core' || $sModule === 'dictionaries'
// force login if needed
$aModuleDelegatedAuthenticationEndpointsList = GetModuleDelegatedAuthenticationEndpoints($sModule);
// If module doesn't have the delegated authentication endpoints list defined, we rely on the conf. param. to decide if we force login or not.
if (is_null($aModuleDelegatedAuthenticationEndpointsList)) {
$bForceLoginWhenNoDelegatedAuthenticationEndpoints = utils::GetConfig()->Get('security.force_login_when_no_delegated_authentication_endpoints_list');
$bForceLoginWhenNoDelegatedAuthenticationEndpoints = !utils::GetConfig()->Get('security.disable_exec_forced_login_for_all_enpoints');
if ($bForceLoginWhenNoDelegatedAuthenticationEndpoints) {
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLoginEx();
}
}
// If module defined a delegated authentication endpoints but not for the current page, we consider that the page is not allowed to be executed without login
if (is_array($aModuleDelegatedAuthenticationEndpointsList) && !in_array($sPage, $aModuleDelegatedAuthenticationEndpointsList)) {
// if module defined a delegated authentication endpoints but not for the current page, we consider that the page is not allowed to be executed without login
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLoginEx();
}
// If user is not logged in, log a warning in the log file as the page is executed without login, which is not recommended for security reason
if (is_null($aModuleDelegatedAuthenticationEndpointsList) && !UserRights::IsLoggedIn()) {
require_once(APPROOT.'/application/startup.inc.php');
// check if user is not logged in, if not log a warning in the log file as the page is executed without login, which is not recommended for security reason
IssueLog::Debug("The '$sPage' page is executed without logging in. This call will be blocked in the future and will likely cause unwanted behaviour in the '$sModule' module. Please define a delegated authentication endpoint for the module, as described at https://www.itophub.io/wiki/page?id=latest:customization:new_extension#security.");
}

View File

@@ -31,7 +31,7 @@ class ModuleInstallation extends DBObject
{
$aParams =
[
"category" => "core,view_in_gui",
"category" => "core,view_in_gui,grant_by_profile",
"key_type" => "autoincrement",
'name_attcode' => ['name', 'version'],
"state_attcode" => "",

View File

@@ -36,7 +36,7 @@ class SetupPage extends NiceWebPage
{
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/setuppage/layout';
protected const BODY_DATA_GUI_TYPE = 'setup';
public const BODY_DATA_GUI_TYPE = 'setup';
public function __construct($sTitle)
{

View File

@@ -0,0 +1,87 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Application\Helper;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\WebPage;
use DBSearch;
use DisplayBlock;
use MetaModel;
use utils;
class SearchHelper
{
/**
* Displays the result of a search request
* @param $oP WebPage Web page for the output
* @param $oFilter DBSearch The search of objects to display
* @param $bSearchForm boolean Whether or not to display the search form at the top the page
* @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
* @param $sFormat string The format to use for the output: csv or html
* @param $bDoSearch bool True to display the search results below the search form
* @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course)
* @throws \CoreException
* @throws \DictExceptionMissingString
*/
public static function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = true, $aParams = []): void
{
//search block
$oBlockForm = null;
if ($bSearchForm) {
$aParams['open'] = $bSearchFormOpen;
if (false === isset($aParams['table_id'])) {
$aParams['table_id'] = 'result_1';
}
if (!empty($sBaseClass)) {
$aParams['baseClass'] = $sBaseClass;
}
$oBlockForm = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams);
if (!$bDoSearch) {
$oBlockForm->Display($oP, 0);
}
}
if ($bDoSearch) {
if (strtolower($sFormat) == 'csv') {
$oBlock = new DisplayBlock($oFilter, 'csv', false);
// Adjust the size of the Textarea containing the CSV to fit almost all the remaining space
$oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block
} else {
$oBlock = new DisplayBlock($oFilter, 'list', false);
// Breadcrumb
//$iCount = $oBlock->GetDisplayedCount();
$sPageId = "ui-search-".$oFilter->GetClass();
$sLabel = MetaModel::GetName($oFilter->GetClass());
$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
}
if ($bSearchForm) {
//add search block
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
if ($sTableId == '') {
$sTableId = 'result_1';
}
$aExtraParams['table_id'] = $sTableId;
$aExtraParams['submit_on_load'] = false;
$oUIBlockForm = $oBlockForm->GetDisplay($oP, 'search_1', $aExtraParams);
// If the class is not high cardinality, we can display the results directly in the same page
if (!utils::IsHighCardinality($oFilter->GetClass())) {
//add result block
$oUIBlock = $oBlock->GetDisplay($oP, $sTableId);
$oUIBlock->AddCSSClasses(['display_block', 'sf_results_area']);
$oUIBlock->AddDataAttribute('target', 'search_results');
$oUIBlockForm->AddSubBlock($oUIBlock);
}
$oP->AddUiBlock($oUIBlockForm);
} else {
$oBlock->Display($oP, 1);
}
}
}
}

View File

@@ -8,8 +8,13 @@ use AttributeFriendlyName;
use AttributeLinkedSet;
use cmdbAbstract;
use cmdbAbstractObject;
use CoreException;
use Dict;
use Exception;
use IssueLog;
use LogChannels;
use Metamodel;
use utils;
/**
* Class DataTableSettings
@@ -130,7 +135,10 @@ class DataTableSettings
*/
public function unserialize($sData)
{
$aData = unserialize($sData);
$aData = utils::Unserialize($sData, ['allowed_classes' => false]);
if (!is_array($aData)) {
throw new CoreException('Wrong data table settings format, expected an array', ['datatable_settings_data' => $aData]);
}
$this->iDefaultPageSize = $aData['iDefaultPageSize'];
$this->aColumns = $aData['aColumns'];
foreach ($this->aClassAliases as $sAlias => $sClass) {
@@ -269,7 +277,19 @@ class DataTableSettings
return null;
}
}
$oSettings->unserialize($pref);
try {
$oSettings->unserialize($pref);
} catch (Exception $e) {
IssueLog::Warning("User table settings corrupted, back to the default values provided by the data model", LogChannels::CONSOLE, [
'table_id' => $sTableId,
'root_cause' => $e->getMessage(),
]);
// unset the preference
appUserPreferences::UnsetPref($oSettings->GetPrefsKey($sTableId));
// use the default values provided by the data model
return null;
}
return $oSettings;
}

View File

@@ -340,8 +340,10 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$aClassAliases = $oSet->GetFilter()->GetSelectedClasses();
$aAuthorizedClasses = [];
foreach ($aClassAliases as $sAlias => $sClassName) {
if ((UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) &&
((count($aDisplayAliases) == 0) || (in_array($sAlias, $aDisplayAliases)))) {
if (
((UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) !== UR_ALLOWED_NO) || ($aExtraParams['display_unauthorized_objects'] ?? false) === true)
&& ((count($aDisplayAliases) == 0) || (in_array($sAlias, $aDisplayAliases)))
) {
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
@@ -520,6 +522,14 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
if ($aData['checked']) {
if ($sAttCode == '_key_') {
if ($bViewLink) {
$sRenderLink = "return row['".$sClassAlias."/hyperlink'];";
if (
($aExtraParams['display_unauthorized_objects'] ?? false) === true
&& UserRights::IsActionAllowed($sClassName, UR_ACTION_READ) !== UR_ALLOWED_YES
) {
$sRenderLink = "return row['".$sClassAlias."/friendlyname'];";
}
$aColumnDefinition[] = [
'description' => $aData['label'],
'object_class' => $sClassName,
@@ -527,7 +537,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
'attribute_code' => $sAttCode,
'attribute_type' => '_key_',
'attribute_label' => MetaModel::GetName($sClassName),
'render' => "return row['".$sClassAlias."/hyperlink'];",
'render' => $sRenderLink,
];
}
@@ -972,6 +982,8 @@ JS;
/** Handler to call when trying to create a new object in modal */
'creation_disallowed',
/** Don't provide the standard object creation feature */
'display_unauthorized_objects',
/** bool Display objects for which the user has no read rights */
];
}
}

View File

@@ -152,6 +152,7 @@ class ActivityPanelFactory
if (false === empty($aRelatedTriggersIDs)) {
// - Prepare query to retrieve events
$oNotifEventsSearch = DBObjectSearch::FromOQL('SELECT EN FROM EventNotification AS EN JOIN Action AS A ON EN.action_id = A.id WHERE EN.trigger_id IN (:triggers_ids) AND EN.object_id = :object_id');
$oNotifEventsSearch->AllowAllData();
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $sObjId]);
$oNotifEventsSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));

View File

@@ -152,7 +152,7 @@ class WebPage implements Page
*/
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/webpage/layout';
protected const BODY_DATA_GUI_TYPE = 'backoffice';
public const BODY_DATA_GUI_TYPE = 'backoffice';
protected $s_title;
protected $s_content;

View File

@@ -74,7 +74,10 @@ class AjaxRenderController
$oSet->SetShowObsoleteData($bShowObsoleteData);
// N°8606 : Check user permissions on the main class
if (UserRights::IsActionAllowed($oSet->GetClass(), UR_ACTION_READ, $oSet) !== UR_ALLOWED_YES) {
if (
UserRights::IsActionAllowed($oSet->GetClass(), UR_ACTION_READ, $oSet) !== UR_ALLOWED_YES
&& ($aExtraParams['display_unauthorized_objects'] ?? false) === false
) {
throw new Exception(Dict::Format('UI:Error:ReadNotAllowedOn_Class', $oSet->GetClass()));
}
@@ -103,7 +106,10 @@ class AjaxRenderController
}
// N°8606 : Check user permissions on the current class
if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) !== UR_ALLOWED_YES) {
if (
UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) !== UR_ALLOWED_YES
&& ($aExtraParams['display_unauthorized_objects'] ?? false) === false
) {
throw new Exception(Dict::Format('UI:Error:ReadNotAllowedOn_Class', $sClass));
}

View File

@@ -26,6 +26,7 @@ use CoreException;
use DBObjectSearch;
use DBObjectSet;
use Dict;
use EventNotificationNewsroom;
use MetaModel;
use SecurityException;
use UserRights;
@@ -358,6 +359,7 @@ JS
// Search for all notifications for the current user
$oSearch = DBObjectSearch::FromOQL('SELECT EventNotificationNewsroom');
$oSearch->AddCondition('contact_id', UserRights::GetContactId(), '=');
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch, ['read' => true, 'date' => false], []);
// Add main content block
@@ -526,6 +528,7 @@ JS
if (utils::IsNotNullOrEmptyString($iContactId)) {
$oSearch = DBObjectSearch::FromOQL('SELECT EventNotificationNewsroom WHERE contact_id = :contact_id AND read = "no"');
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch, [], ['contact_id' => $iContactId]);
while ($oMessage = $oSet->Fetch()) {
@@ -539,7 +542,7 @@ $sMessage
HTML;
$sIcon = $oMessage->Get('icon') !== null ?
$oMessage->Get('icon')->GetDisplayURL('EventNotificationNewsroom', $oMessage->GetKey(), 'icon') :
$oMessage->Get('icon')->GetDisplayURL(EventNotificationNewsroom::class, $oMessage->GetKey(), 'icon') :
Branding::GetCompactMainLogoAbsoluteUrl();
$aMessages[] = [
'id' => $oMessage->GetKey(),
@@ -576,6 +579,7 @@ HTML;
if (utils::IsNotNullOrEmptyString($iContactId)) {
$oSearch = DBObjectSearch::FromOQL('SELECT EventNotificationNewsroom WHERE contact_id = :contact_id AND read = "no"');
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch, [], ['contact_id' => $iContactId]);
while ($oEvent = $oSet->Fetch()) {
@@ -605,7 +609,7 @@ HTML;
$sEventId = utils::ReadParam('event_id', 0);
if ($sEventId > 0) {
try {
$oEvent = MetaModel::GetObject('EventNotificationNewsroom', $sEventId);
$oEvent = MetaModel::GetObject(EventNotificationNewsroom::class, $sEventId, true, true);
if ($oEvent !== null && $oEvent->Get('contact_id') === UserRights::GetContactId()) {
$oEvent->Set('read', 'yes');
$oEvent->SetCurrentDate('read_date');

View File

@@ -117,6 +117,7 @@ class NotificationsRepository
protected function PrepareSearchForNotificationsByContact(int $iContactId, array $aNotificationIds = []): DBSearch
{
$oSearch = DBObjectSearch::FromOQL("SELECT EventNotificationNewsroom WHERE contact_id = :contact_id");
$oSearch->AllowAllData();
$aParams = [
"contact_id" => $iContactId,
];

View File

@@ -7,6 +7,6 @@ oWidget{{ oUIBlock.oUILinksDirectWidget.GetInputId() }} = $('#{{ oUIBlock.oUILin
input_name: '{{ oUIBlock.sInputName }}',
submit_to: '{{ oUIBlock.sSubmitUrl }}',
oWizardHelper: {{ oUIBlock.sWizHelper }},
do_search: '{{ oUIBlock.sJSDoSearch }}'
do_search: {{ oUIBlock.sJSDoSearch }}
});
{% endapply %}

View File

@@ -49,7 +49,7 @@
{% endfor %}
{% endblock %}
</head>
<body data-gui-type="{{ aPage.sBodyDataGuiType|default('backoffice') }}">
<body data-gui-type="{{ aPage.sBodyDataGuiType|default(constant('Combodo\\iTop\\Application\\WebPage\\WebPage::BODY_DATA_GUI_TYPE')) }}">
{% if aPage.isPrintable %}<div class="printable-content" style="width: 27.7cm;">{% endif %}
{% block iboPageBodyHtml %}
<div id="ibo-page-container">

View File

@@ -26,14 +26,14 @@ class LoginWebPageTest extends ItopDataTestCase
$this->BackupConfiguration();
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
$this->RecurseRmdir($sFolderPath);
}
mkdir($sFolderPath);
$this->RecurseCopy(__DIR__.'/extension-with-delegated-authentication-endpoints-list', $sFolderPath);
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
$this->RecurseRmdir($sFolderPath);
}
mkdir($sFolderPath);
$this->RecurseCopy(__DIR__.'/extension-without-delegated-authentication-endpoints-list', $sFolderPath);
@@ -81,8 +81,7 @@ class LoginWebPageTest extends ItopDataTestCase
public function testUserCanAccessAnyFile()
{
// generate random login
$sUserLogin = 'user-'.date('YmdHis');
$sUserLogin = 'user-'.uniqid();
$this->CreateUser($sUserLogin, self::$aURP_Profiles['Service Desk Agent'], self::PASSWORD);
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
@@ -102,7 +101,7 @@ class LoginWebPageTest extends ItopDataTestCase
public function testWithoutDelegatedAuthenticationEndpointsListWithForceLoginConf()
{
@chmod($this->oConfig->GetLoadedFile(), 0770);
$this->oConfig->Set('security.force_login_when_no_delegated_authentication_endpoints_list', true, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
$this->oConfig->Set('security.disable_exec_forced_login_for_all_enpoints', false, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
$this->oConfig->WriteToFile();
@chmod($this->oConfig->GetLoadedFile(), 0444);
$sPageContent = $this->CallItopUri(

View File

@@ -1470,4 +1470,19 @@ abstract class ItopDataTestCase extends ItopTestCase
@chmod($sConfigPath, 0440);
@unlink($this->sConfigTmpBackupFile);
}
protected function AddLoginModeAndSaveConfiguration(string $sLoginMode): void
{
$aAllowedLoginTypes = $this->oiTopConfig->GetAllowedLoginTypes();
if (!in_array($sLoginMode, $aAllowedLoginTypes)) {
$aAllowedLoginTypes[] = $sLoginMode;
$this->oiTopConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
$this->SaveItopConfFile();
}
}
protected function SaveItopConfFile(): void
{
@chmod($this->oiTopConfig->GetLoadedFile(), 0770);
$this->oiTopConfig->WriteToFile();
@chmod($this->oiTopConfig->GetLoadedFile(), 0440);
}
}

View File

@@ -668,7 +668,7 @@ abstract class ItopTestCase extends KernelTestCase
}
curl_setopt($ch, CURLOPT_URL, $sUrl);
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
curl_setopt($ch, CURLOPT_POST, $aCurlOptions[CURLOPT_POST] ?? 1);// set post data to true
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Force disable of certificate check as most of dev / test env have a self-signed certificate
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
@@ -708,6 +708,16 @@ abstract class ItopTestCase extends KernelTestCase
{
$sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
// Add PHP version in header to be able to handle Docker dev environments with automatic PHP version detection (instead of hardcoding the PHP version in the app_root_url)
$sPhpVersion = PHP_VERSION;
$aPhpVersionParts = explode('.', $sPhpVersion);
$sPhpVersionHeaderValue = ($aPhpVersionParts[0] ?? '0').($aPhpVersionParts[1] ?? '0');
$aCurlOptions = $aCurlOptions ?? [];
$aCurlOptions[CURLOPT_HTTPHEADER] = array_merge(
$aCurlOptions[CURLOPT_HTTPHEADER] ?? [],
['X-PHP-Version: '.$sPhpVersionHeaderValue]
);
return $this->CallUrl($sUrl, $aPostFields, $aCurlOptions, $bXDebugEnabled);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Application\Helper;
use Combodo\iTop\Application\Helper\SearchHelper;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DBSearch;
use MetaModel;
class SearchHelperTest extends ItopDataTestCase
{
protected static array $aHighCardinalityClasses = [];
protected static bool $bSearchManualSubmit = false;
protected function setUp(): void
{
parent::setUp();
self::$aHighCardinalityClasses = MetaModel::GetConfig()->Get('high_cardinality_classes');
self::$bSearchManualSubmit = MetaModel::GetConfig()->Get('search_manual_submit');
}
protected function tearDown(): void
{
parent::tearDown();
MetaModel::GetConfig()->Set('high_cardinality_classes', static::$aHighCardinalityClasses);
MetaModel::GetConfig()->Set('search_manual_submit', static::$bSearchManualSubmit);
}
public function testDisplaySearchSetWithNoHighCardinalityClassesAddsResultSubBlock(): void
{
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
MetaModel::GetConfig()->Set('search_manual_submit', false);
$oP = new iTopWebPage('SearchHelperTest');
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
SearchHelper::DisplaySearchSet($oP, $oFilter);
$oContentLayout = $oP->GetContentLayout();
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
$this->assertTrue($oSearchBlock->HasSubBlock('result_1'));
if (ob_get_level() > 0) {
ob_end_clean();
}
}
public function testDisplaySearchSetWithHighCardinalityClassesDoesNotAddResultSubBlock(): void
{
MetaModel::GetConfig()->Set('high_cardinality_classes', ['UserRequest']);
MetaModel::GetConfig()->Set('search_manual_submit', false);
$oP = new iTopWebPage('SearchHelperTest');
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
SearchHelper::DisplaySearchSet($oP, $oFilter);
$oContentLayout = $oP->GetContentLayout();
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
if (ob_get_level() > 0) {
ob_end_clean();
}
}
public function testDisplaySearchSetWithSearchManualSubmitAndWithoutHighCardinalityClassesDoesNotAddResultSubBlock(): void
{
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
MetaModel::GetConfig()->Set('search_manual_submit', true);
$oP = new iTopWebPage('SearchHelperTest');
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
SearchHelper::DisplaySearchSet($oP, $oFilter);
$oContentLayout = $oP->GetContentLayout();
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
if (ob_get_level() > 0) {
ob_end_clean();
}
}
}

View File

@@ -645,4 +645,54 @@ SCSS;
[ '/var/www/html/iTop/css/ui-lightness/images/ui-icons_222222_256x240.png', '/var/www/html/iTop/env-production//branding/themes/light-grey//../../../../css/ui-lightness/images/ui-icons_222222_256x240.png' ],
];
}
/**
* @param $aThemeParameters
* @param $bSetupCompilationTimestamp
* @param $aExpectedClonedParameters
* @dataProvider CloneParameterParameterOverloadProvider
*/
public function testCloneParameterParameterOverload($aThemeParameters, $bSetupCompilationTimestamp, $aExpectedClonedParameters)
{
$aClonedParameters = ThemeHandler::CloneThemeParameterAndIncludeVersion($aThemeParameters, $bSetupCompilationTimestamp, [APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/imports/']);
$this->assertEquals($aExpectedClonedParameters, $aClonedParameters);
}
public function CloneParameterParameterOverloadProvider()
{
return [
"empty parameters" => [
'parameters' => [],
'timestamp' => '1',
'expected' => [
'$version' => '1',
],
],
"parameters without variables" => [
'parameters' => [
'variable_imports' => ['file1' => 'variable_imports.scss'],
'utility_imports' => ['util1' => 'path2'],
'stylesheets' => ['style1' => 'path3'],
],
'timestamp' => '2',
'expected' => [
'var1' => 'value1',
'var2' => 'value2',
'$version' => '2',
],
],
"parameters with variables overload" => [
'parameters' => [
'variables' => ['var1' => 'value2'],
'variable_imports' => ['file1' => 'variable_imports.scss'],
],
'timestamp' => '3',
'expected' => [
'var1' => 'value2',
'var2' => 'value2',
'$version' => '3',
],
],
];
}
}

View File

@@ -0,0 +1,2 @@
$var1: value1;
$var2: value2;

View File

@@ -0,0 +1,193 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DBObjectSearch;
use DBObjectSet;
use DBSearch;
use lnkFunctionalCIToTicket;
use MetaModel;
use ormLinkSet;
use UserRequest;
use UserRights;
class DBSearchFilterJoinTest extends ItopDataTestCase
{
private const RESTRICTED_PROFILE = 'Configuration Manager';
private $aData = [];
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('application/startup.inc.php');
$this->aData = $this->CreateDBSearchFilterTestData();
DBSearch::EnableQueryCache(false, false);
$this->LoginRestrictedUser($this->aData['allowed_org_id'], self::RESTRICTED_PROFILE);
}
protected function tearDown(): void
{
parent::tearDown();
}
/**
* @dataProvider JoinedAndNestedOqlProvider
*/
public function testDBSearchFilterAppliedToJoinsWhenEnabled(string $sOql, int $iExpectedCount): void
{
$this->EnableJoinFilterConfig(true);
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
$oSet = new \DBObjectSet($oSearch);
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
$this->assertEquals($iExpectedCount, $oSet->Count());
}
/**
* @dataProvider JoinedAndNestedOqlProvider
*/
public function testDBSearchFilterAppliedToJoinsWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
{
$this->EnableJoinFilterConfig(false);
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
$oSet = new \DBObjectSet($oSearch);
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
}
/**
* @dataProvider JoinedAndNestedOqlProvider
*/
public function testAllowAllDataBypassesDBSearchFilterWhenEnabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
{
$this->EnableJoinFilterConfig(true);
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
$oSearch->AllowAllData();
$oSet = new \DBObjectSet($oSearch);
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
}
/**
* @dataProvider JoinedAndNestedOqlProvider
*/
public function testAllowAllDataBypassesDBSearchFilterWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
{
$this->EnableJoinFilterConfig(false);
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
$oSearch->AllowAllData();
$oSet = new \DBObjectSet($oSearch);
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
}
public function JoinedAndNestedOqlProvider(): array
{
return [
'join-filter-on-org' => [
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org",
'expected_filtered_count' => 0,
'expected_unfiltered_count' => 1,
],
'nested-in-select' => [
'oql' => "SELECT OSF FROM OSFamily AS OSF WHERE OSF.id IN (SELECT OSF1 FROM OSFamily AS OSF1 JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF1.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org)",
'expected_filtered_count' => 0,
'expected_unfiltered_count' => 1,
],
'userrequest-join-person-org' => [
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = VM.id JOIN UserRequest AS UR ON L.ticket_id = UR.id JOIN Person AS P ON UR.caller_id = P.id JOIN Organization AS O ON P.org_id = O.id WHERE O.name = :denied_org",
'expected_filtered_count' => 0,
'expected_unfiltered_count' => 1,
],
'union-join-filter-on-org' => [
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org UNION SELECT OSF2 FROM OSFamily AS OSF2 JOIN VirtualMachine AS VM2 ON VM2.osfamily_id = OSF2.id JOIN Organization AS O2 ON VM2.org_id = O2.id WHERE O2.name = :allowed_org",
'expected_filtered_count' => 1,
'expected_unfiltered_count' => 2,
],
];
}
private function EnableJoinFilterConfig(bool $bEnabled): void
{
$oConfig = MetaModel::GetConfig();
$oConfig->Set('security.disable_joined_classes_filter', !$bEnabled);
}
private function CreateDBSearchFilterTestData(): array
{
$sSuffix = 'DBSearchFilterJoinTest';
$sAllowedOrgName = 'DBSearchFilterAllowedOrg-'.$sSuffix;
$iAllowedOrgId = $this->GivenObjectInDB('Organization', [
'name' => $sAllowedOrgName,
]);
$this->debug("Org allowed id: $iAllowedOrgId");
$sDeniedOrgName = 'DBSearchFilterDeniedOrg-'.$sSuffix;
$iDeniedOrgId = $this->GivenObjectInDB('Organization', [
'name' => $sDeniedOrgName,
]);
$this->debug("Org denied id: $iDeniedOrgId");
$iDeniedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
'name' => 'DBSearchFilterOsFamilyDenied-'.$sSuffix,
]);
$iAllowedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
'name' => 'DBSearchFilterOsFamilyAllowed-'.$sSuffix,
]);
$iDeniedVMId = $this->GivenObjectInDB('VirtualMachine', [
'name' => 'DBSearchFilterVmDenied-'.$sSuffix,
'org_id' => $iDeniedOrgId,
'osfamily_id' => $iDeniedOsFamilyId,
'virtualhost_id' => 1,
]);
$iVirtualHostId = $this->GivenObjectInDB('Hypervisor', [
'name' => 'DBSearchFilterVHost-'.$sSuffix,
'org_id' => $iAllowedOrgId,
]);
$this->GivenObjectInDB('VirtualMachine', [
'name' => 'DBSearchFilterVmAllowed-'.$sSuffix,
'org_id' => $iAllowedOrgId,
'osfamily_id' => $iAllowedOsFamilyId,
'virtualhost_id' => $iVirtualHostId,
]);
$oDeniedPerson = $this->CreatePerson('Denied-'.$sSuffix, $iDeniedOrgId);
$oUserRequest = $this->CreateUserRequest('Denied'.$sSuffix, [
'caller_id' => $oDeniedPerson->GetKey(),
'org_id' => $iDeniedOrgId,
]);
// Add Virtual Machine to UserRequest lnk
$oLinkSet = new ormLinkSet(UserRequest::class, 'functionalcis_list', DBObjectSet::FromScratch(lnkFunctionalCIToTicket::class));
$oLink = MetaModel::NewObject(lnkFunctionalCIToTicket::class, ['functionalci_id' => $iDeniedVMId]);
$oLinkSet->AddItem($oLink);
$oUserRequest->Set('functionalcis_list', $oLinkSet);
$oUserRequest->DBUpdate();
return [
'allowed_org_id' => $iAllowedOrgId,
'allowed_org_name' => $sAllowedOrgName,
'denied_org_name' => $sDeniedOrgName,
];
}
private function LoginRestrictedUser(int $iAllowedOrgId, string $sProfileName): void
{
$sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($iAllowedOrgId, self::$aURP_Profiles[$sProfileName]);
UserRights::Login($sLogin);
}
}

View File

@@ -214,14 +214,14 @@ class UserRightsTest extends ItopDataTestCase
'User Portal UserRequest read' => [2, ['class' => 'UserRequest', 'action' => 1, 'res' => true]],
'User Portal URP_UserProfile read' => [2, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]],
'User Portal UserLocal read' => [2, ['class' => 'UserLocal', 'action' => 1, 'res' => false]],
'User Portal ModuleInstallation read' => [2, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]],
'User Portal ModuleInstallation read' => [2, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => false]],
/* Configuration manager (1 = UR_ACTION_READ) */
'Configuration manager FunctionalCI read' => [3, ['class' => 'FunctionalCI', 'action' => 1, 'res' => true]],
'Configuration manager UserRequest read' => [3, ['class' => 'UserRequest', 'action' => 1, 'res' => true]],
'Configuration manager URP_UserProfile read' => [3, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]],
'Configuration manager UserLocal read' => [3, ['class' => 'UserLocal', 'action' => 1, 'res' => false]],
'Configuration manager ModuleInstallation read' => [3, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]],
'Configuration manager ModuleInstallation read' => [3, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => false]],
];
}
@@ -267,14 +267,14 @@ class UserRightsTest extends ItopDataTestCase
'User Portal UserRequest' => [2, ['class' => 'UserRequest', 'action' => 2, 'res' => true]],
'User Portal URP_UserProfile' => [2, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]],
'User Portal UserLocal' => [2, ['class' => 'UserLocal', 'action' => 2, 'res' => false]],
'User Portal ModuleInstallation' => [2, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]],
'User Portal ModuleInstallation' => [2, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => false]],
/* Configuration manager (2 = UR_ACTION_MODIFY) */
'Configuration manager FunctionalCI' => [3, ['class' => 'FunctionalCI', 'action' => 2, 'res' => true]],
'Configuration manager UserRequest' => [3, ['class' => 'UserRequest', 'action' => 2, 'res' => false]],
'Configuration manager URP_UserProfile' => [3, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]],
'Configuration manager UserLocal' => [3, ['class' => 'UserLocal', 'action' => 2, 'res' => false]],
'Configuration manager ModuleInstallation' => [3, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]],
'Configuration manager ModuleInstallation' => [3, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => false]],
];
}

View File

@@ -7,14 +7,36 @@
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Application\WebPage\CaptureWebPage;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ormDocument;
use UserRights;
/**
* Tests of the ormDocument class
*/
class ormDocumentTest extends ItopDataTestCase
{
private const RESTRICTED_PROFILE = 'Configuration Manager';
private int $iUserOrg;
private int $iOrgDifferentFromUser;
protected function setUp(): void
{
parent::setUp();
$this->iUserOrg = $this->GivenObjectInDB('Organization', [
'name' => 'UserOrg',
]);
$this->iOrgDifferentFromUser = $this->GivenObjectInDB('Organization', [
'name' => 'OrgDifferentFromUser',
]);
$this->LoginRestrictedUser($this->iUserOrg, self::RESTRICTED_PROFILE);
$this->ResetMetaModelQueyCacheGetObject();
}
/**
* @inheritDoc
*/
@@ -139,4 +161,107 @@ class ormDocumentTest extends ItopDataTestCase
],
];
}
/**
* Test that DownloadDocument enforces rights for documents
*
* @dataProvider DownloadDocumentRightsProvider
*/
public function testDownloadDocumentDifferentOrg(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass)
{
$iDeniedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iOrgDifferentFromUser, $sData, $sFileName, $sHostClass);
$oPageDenied = new CaptureWebPage();
ormDocument::DownloadDocument($oPageDenied, $sTargetClass, $iDeniedDocumentId, $sAttCode);
$sDeniedHtml = (string) $oPageDenied->GetHtml();
$this->assertStringContainsString(
'the object does not exist or you are not allowed to view it',
$sDeniedHtml,
'Expected error message when rights are missing.'
);
$this->assertStringNotContainsString($sData, $sDeniedHtml, 'Unexpected file data present when rights are missing.');
}
/**
* Test that DownloadDocument allows to retrieve document with the same org (or host object org)
*
* @dataProvider DownloadDocumentRightsProvider
*/
public function testDownloadDocumentSameOrg(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass)
{
$iAllowedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iUserOrg, $sData, $sFileName, $sHostClass);
$oPageAllowed = new CaptureWebPage();
ormDocument::DownloadDocument($oPageAllowed, $sTargetClass, $iAllowedDocumentId, $sAttCode);
$sAllowedHtml = (string) $oPageAllowed->GetHtml();
$this->assertStringContainsString($sData, $sAllowedHtml, 'Expected file data present when rights are sufficient.');
$this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sAllowedHtml, 'Unexpected error message when rights are sufficient.');
}
public function DownloadDocumentRightsProvider(): array
{
return [
'DocumentFile' => [
'class' => 'DocumentFile',
'data_attribute_id' => 'file',
'data' => 'document_data',
'file_name' => 'document.txt',
'host_class' => null],
'Attachment' => [
'class' => 'Attachment',
'data_attribute_id' => 'contents',
'data' => 'attachment_data',
'file_name' => 'attachment.txt',
'host_class' => 'UserRequest'],
];
}
/**
* Helper to avoid duplicating object creation in tests
* Created objects and host objects depending on the Document class
* @param string $sTargetClass
* @param string $sAttCode
* @param int $iOrgId
* @param string $sData
* @param string $sFileName
* @param string|null $sHostClass
* @return int
* @throws \Exception
*/
private function CreateDownloadTargetInOrg(string $sTargetClass, string $sAttCode, int $iOrgId, string $sData, string $sFileName, ?string $sHostClass): int
{
if ($sTargetClass === 'DocumentFile') {
return $this->GivenObjectInDB($sTargetClass, [
'name' => 'UnitTestDocFile_'.uniqid(),
'org_id' => $iOrgId,
$sAttCode => new ormDocument($sData, 'text/plain', $sFileName),
]);
}
if ($sTargetClass === 'Attachment') {
$iHostId = $this->GivenObjectInDB($sHostClass, [
'title' => 'UnitTestUserRequest_'.uniqid(),
'org_id' => $iOrgId,
'description' => 'A user request for testing attachment download rights',
]);
return $this->GivenObjectInDB('Attachment', [
'item_class' => $sHostClass,
'item_id' => $iHostId,
$sAttCode => new ormDocument($sData, 'text/plain', $sFileName),
]);
}
throw new \Exception("Unsupported target class: $sTargetClass");
}
private function LoginRestrictedUser(int $iAllowedOrgId, string $sProfileName): void
{
if (UserRights::IsLoggedIn()) {
UserRights::Logoff();
}
$sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($iAllowedOrgId, self::$aURP_Profiles[$sProfileName]);
UserRights::Login($sLogin);
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Pages;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Dict;
use UserLocal;
use UserRequest;
class AjaxRenderTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
public const AUTHENTICATION_PASSWORD = "tagada-Secret,007";
private static string $sLogin;
private static int $iTicketId;
protected function setUp(): void
{
parent::setUp();
$this->BackupConfiguration();
$this->oiTopConfig->Set('log_level_min', 'Error');
$this->oiTopConfig->Set('login_debug', true);
$this->CreateTestOrganization();
// Add URL authentication mode
$this->AddLoginModeAndSaveConfiguration('url');
// Create ticket
$description = date('dmY H:i:s');
$oTicket = $this->createObject('UserRequest', [
'org_id' => $this->getTestOrgId(),
"title" => "Houston, got a problem",
"description" => $description,
]);
self::$iTicketId = $oTicket->GetKey();
}
// Test that if a user with the right permissions tries to acquire the lock on a ticket, it succeeds and returns the correct success message
public function testAcquireLockSuccess(): void
{
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
$this->assertStringContainsString('"success":true', $sOutput);
}
// Test that if a user tries to acquire the lock on an object that does not exist, it fails and logs the correct error message
public function testAcquireLockFailsIfObjectDoesNotExist(): void
{
// Create a user with Support Agent Profile
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
// Try to acquire the lock on a non-existent object
$sOutput = $this->AcquireLockAsUser(self::$sLogin, 99999999);
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
// Check that the error log contains the expected error message about the object not existing
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
}
// Test that if a user tries to acquire the lock on an object for which they don't have modification rights, it fails and logs the correct error message
public function testAcquireLockFailsIfUserHasNoModifyRights(): void
{
// Create a user with a profile without modification rights on UserRequest
$this->CreateUserWithProfile(self::$aURP_Profiles['Configuration Manager']);
// Try to acquire the lock on the ticket
$sOutput = $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
// The user should not have the rights to acquire the lock, and an error should be logged
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
}
// Test that if a user tries to acquire the lock on an object that belongs to another organization, it fails and logs the correct error message
public function testAcquireLockFailsIfObjectInOtherOrg(): void
{
// Create an organization and a ticket in this organization
$iOtherOrgId = $this->createObject('Organization', ['name' => 'OtherOrg'])->GetKey();
$oTicket = $this->createObject('UserRequest', [
'org_id' => $iOtherOrgId,
'title' => 'Ticket autre org',
'description' => 'Test',
]);
// Create a user who only has access to the main test organization
$oUser = $this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
$oAllowedOrgList = $oUser->Get('allowed_org_list');
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $this->getTestOrgId()]);
$oAllowedOrgList->AddItem($oUserOrg);
$oUser->Set('allowed_org_list', $oAllowedOrgList);
$oUser->DBWrite();
// Try to acquire the lock on the ticket of the other organization
$sOutput = $this->AcquireLockAsUser(self::$sLogin, $oTicket->GetKey());
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
// The user should not have access to the ticket of the other organization, so an error should be logged
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
}
// Test that if a user has already acquired the lock on an object, another user cannot acquire it and gets the correct error message
public function testAcquireLockFailsIfAlreadyLockedByAnotherUser(): void
{
// First, acquire the lock with a user (User A)
$this->CreateSupportAgentUserAndAcquireLock();
$sUserALogin = self::$sLogin;
// Create a second user (User B) who tries to acquire the lock
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
// The second user should not be able to acquire the lock, and the output should contain the correct error message indicating that the object is already locked by User A
$this->assertStringContainsString('"success":false', $sOutput);
$this->assertStringContainsString('"message":"'.Dict::Format('UI:CurrentObjectIsSoftLockedBy_User', $sUserALogin).'"', $sOutput);
}
// Helper method to create a user with Support Agent profile and acquire the lock on the ticket
private function CreateSupportAgentUserAndAcquireLock(): string
{
// Create a user with Support Agent Profile
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
return $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
}
// Helper method to create a user with a specific profile
private function CreateUserWithProfile(int $iProfileId): UserLocal
{
self::$sLogin = uniqid('AjaxRenderTest');
return $this->CreateContactlessUser(self::$sLogin, $iProfileId, self::AUTHENTICATION_PASSWORD);
}
// Helper method to acquire the lock on a ticket as a specific user
private function AcquireLockAsUser(string $sLogin, int $iTicketId): string
{
$aGetFields = [
'operation' => 'acquire_lock',
'auth_user' => $sLogin,
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
'obj_class' => UserRequest::class,
'obj_key' => $iTicketId,
];
return $this->CallItopUri(
"pages/ajax.render.php?".http_build_query($aGetFields),
[],
[
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
CURLOPT_POST => 0,
]
);
}
// Returns the last lines of the error log containing only errors (Error level)
private function GetErrorLogLastLines(string $sErrorLogPath, int $iLineNumbers = 1): string
{
if (!file_exists($sErrorLogPath)) {
return '';
}
$aLines = file($sErrorLogPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Keep only lines containing '| Error |'
$aErrorLines = array_filter($aLines, function ($line) {
return preg_match('/\|\s*Error\s*\|/', $line);
});
// Return the last requested lines
return implode("\n", array_slice($aErrorLines, -$iLineNumbers));
}
}