Compare commits

..

30 Commits

Author SHA1 Message Date
Stephen Abello
82baa2e5cb Update js/forms-json-utils.js
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-01 15:38:45 +02:00
Stephen Abello
7cd7ba74d4 Bring back lost methods 2026-04-01 15:34:04 +02:00
Stephen Abello
b23b336d60 N°8758 - Fix mandatory caselog in transition requiring double confirmation 2026-04-01 15:26:10 +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
Lenaick
511dabe2b0 N°8834 - Updated the PHP version to 8.4 for the CI in the commit 2026-03-25 17:43:39 +01:00
Lenaick
0c517f254c N°9101 - Improve HTML markup for end-to-end tests automation (#856) 2026-03-25 17:20:28 +01:00
Stephen Abello
c56c7a1f9d Fix CI by fixing code style 2026-03-25 10:25:37 +01:00
Stephen Abello
fb2f0f1447 N°9328 - Add scssphp compatibility with PHP 8.4 (#851) 2026-03-25 09:58:05 +01:00
Lenaick
b3223eb9b6 N°8606 - Check user permissions in search operation of ajax.render.php (#836) 2026-03-24 08:52:22 +01:00
Benjamin DALSASS
458a996c29 N°8612 - force authentication for inline image endpoints
- ajax.render dict route needs to be reached without login authentication
2026-03-23 15:50:47 +01:00
Anne-Catherine
c61b21559c N°8692 - Notification - placeholder attributesubitem (#778) 2026-03-23 15:33:50 +01:00
jf-cbd
ed33238750 Merge remote-tracking branch 'origin/support/3.2' into support/3.2 2026-03-20 16:31:44 +01:00
jf-cbd
272678b8cd N°9361 - Indicate to itop admin concerned by 8543 that changes could be required in extension 2026-03-20 16:30:53 +01:00
Benjamin Dalsass
170014e8f0 N°9232 - Information Disclosure (#850) 2026-03-20 14:35:05 +01:00
Molkobain
006f666089 N°8554 - Fix impossible installation of portal new look via iTop Hub (#846) 2026-03-18 19:59:42 +01:00
Stephen Abello
2a16143e53 N°9229 - Modernize search foreign keys code with built in JS tools (#847)
* N°9229 - Modernize search foreign keys code with built in JS tools

* N°9229 - Allow modals to have button id specified

* N°9229 - Remove the modal instead of only destroying it

* N°9229 - Remove dead code

* Update js/searchformforeignkeys.js

* Add robustness to modals button id
2026-03-18 15:23:52 +01:00
jf-cbd
eabbe2f00b N°8543 - best practices 2026-03-18 10:09:25 +01:00
Molkobain
58790bc352 N°8597 - Fix special characters being escaped for BrowseBrick items in "Tree" mode (#845)
* N°8597 - Fix special characters being escaped for BrowseBrick items in "Tree" mode

* N°8597 - Fix forgotten use case for "Tree" mode for intermediate levels
2026-03-17 15:02:20 +01:00
Lenaick
28db230697 N°9233 - Check user access before acquiring lock on object (#844) 2026-03-16 17:07:26 +01:00
jf-cbd
4fe61cbdc7 N°8543 - Add checks on exec.php (#835) 2026-03-16 17:06:37 +01:00
Benjamin Dalsass
e2994b645b N°8612 inline images to base64 (#826) 2026-03-16 08:36:37 +01:00
89 changed files with 1654 additions and 781 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

@@ -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

@@ -27,6 +27,9 @@ require_once(APPROOT.'/application/displayblock.class.inc.php');
class UISearchFormForeignKeys
{
private $m_sRemoteClass;
private $m_iInputId;
public function __construct($sTargetClass, $iInputId = null)
{
$this->m_sRemoteClass = $sTargetClass;
@@ -40,7 +43,7 @@ class UISearchFormForeignKeys
*
* @throws \Exception
*/
public function ShowModalSearchForeignKeys($oPage, $sTitle)
public function ShowModalSearchForeignKeys($oPage)
{
$oFilter = new DBObjectSearch($this->m_sRemoteClass);
@@ -60,8 +63,6 @@ class UISearchFormForeignKeys
]
));
$sEmptyList = Dict::S('UI:Message:EmptyList:UseSearchForm');
$sCancel = Dict::S('UI:Button:Cancel');
$sAdd = Dict::S('UI:Button:Add');
$oPage->add(
<<<HTML
@@ -73,39 +74,6 @@ class UISearchFormForeignKeys
</form>
HTML
);
$oPage->add_ready_script(
<<<JS
$('#dlg_{$this->m_iInputId}').dialog({
width: $(window).width()*0.8,
height: $(window).height()*0.8,
autoOpen: false,
modal: true,
resizeStop: oForeignKeysWidget{$this->m_iInputId}.UpdateSizes,
buttons: [
{
text: Dict.S('UI:Button:Cancel'),
class: "cancel ibo-is-alternative ibo-is-neutral",
click: function() {
$('#dlg_{$this->m_iInputId}').dialog('close');
}
},
{
text: Dict.S('UI:Button:Add'),
id: 'btn_ok_{$this->m_iInputId}',
class: "ok ibo-is-regular ibo-is-primary",
click: function() {
oForeignKeysWidget{$this->m_iInputId}.DoAddObjects(this.id);
}
},
],
});
$('#dlg_{$this->m_iInputId}').dialog('option', {title:'$sTitle'});
$('#SearchFormToAdd_{$this->m_iInputId} form').on('submit.uilinksWizard', oForeignKeysWidget{$this->m_iInputId}.SearchObjectsToAdd);
$('#SearchFormToAdd_{$this->m_iInputId}').on('resize', oForeignKeysWidget{$this->m_iInputId}.UpdateSizes);
JS
);
}
public function GetFullListForeignKeysFromSelection($oPage, $oFullSetFilter)
@@ -119,31 +87,4 @@ JS
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
}
}
/**
* Search for objects to be linked to the current object (i.e "remote" objects)
*
* @param WebPage $oP The page used for the output (usually an AjaxWebPage)
* @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass
*
* @throws \Exception
*/
public function ListResultsSearchForeignKeys(WebPage $oP, $sRemoteClass = '')
{
if ($sRemoteClass != '') {
// assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass));
$oFilter = new DBObjectSearch($sRemoteClass);
} else {
// No remote class specified use the one defined in the linkedset
$oFilter = new DBObjectSearch($this->m_sRemoteClass);
}
$oBlock = new DisplayBlock($oFilter, 'list', false);
$oBlock->Display(
$oP,
"ResultsToAdd_{$this->m_iInputId}",
['menu' => false, 'cssCount' => "#count_{$this->m_iInputId}", 'selection_mode' => true, 'table_id' => "add_{$this->m_iInputId}"]
);
}
}

View File

@@ -122,6 +122,11 @@ class utils
* @since 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_VARIABLE_NAME = 'variable_name';
/**
* @var string For module codes (e.g. `itop-portal-base`, `combodo-webhook-integration`, `some-module-code-x.y`, ...)
* @since 3.2.3 3.3.0 N°8554
*/
public const ENUM_SANITIZATION_FILTER_MODULE_CODE = 'module_code';
/**
* @var string
* @since 2.7.10 3.0.0
@@ -393,6 +398,7 @@ class utils
* @since 2.7.10 N°6606 use the utils::ENUM_SANITIZATION_* const
* @since 2.7.10 N°6606 new case for ENUM_SANITIZATION_FILTER_PHP_CLASS
* @since 3.2.1-1 N°8242 Allow value to be an array for every filter
* @since 3.2.3 3.3.0 N°8554 new case for ENUM_SANITIZATION_FILTER_MODULE_CODE
*
* @link https://www.php.net/manual/en/filter.filters.sanitize.php PHP sanitization filters
*/
@@ -480,7 +486,7 @@ class utils
);
break;
// For XML / HTML node id selector
// For XML / HTML node selector
case static::ENUM_SANITIZATION_FILTER_ELEMENT_SELECTOR:
$retValue = filter_var(
$value,
@@ -493,6 +499,15 @@ class utils
$retValue = preg_replace('/[^a-zA-Z0-9_]/', '', $value);
break;
case static::ENUM_SANITIZATION_FILTER_MODULE_CODE:
// Module codes allow all alphabets letters, numbers, dash and dot characters
$retValue = filter_var(
$value,
FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[\p{L}\d.-]+$/u']]
);
break;
// For URL
case static::ENUM_SANITIZATION_FILTER_URL:
$retValue = filter_var($value, FILTER_SANITIZE_URL);
@@ -1440,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;

View File

@@ -19,7 +19,7 @@
"pear/archive_tar": "~1.4.14",
"pelago/emogrifier": "^7.2.0",
"psr/log": "^3.0.0",
"scssphp/scssphp": "^1.12.1",
"scssphp/scssphp": "dev-combodo/1.x",
"symfony/console": "~6.4.0",
"symfony/dotenv": "~6.4.0",
"symfony/framework-bundle": "~6.4.0",
@@ -43,6 +43,10 @@
{
"type": "vcs",
"url": "https://github.com/EsupPortail/phpCAS"
},
{
"type": "vcs",
"url": "https://github.com/combodo-itop-libs/scssphp"
}
],
"suggest": {

30
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ceac38f6033afe07b7ab977fa39fe348",
"content-hash": "eebbdc6c10a479b0e62fc18d88496f5c",
"packages": [
{
"name": "apereo/phpcas",
@@ -1588,16 +1588,16 @@
},
{
"name": "scssphp/scssphp",
"version": "v1.13.0",
"version": "dev-combodo/1.x",
"source": {
"type": "git",
"url": "https://github.com/scssphp/scssphp.git",
"reference": "63d1157457e5554edf00b0c1fabab4c1511d2520"
"url": "https://github.com/combodo-itop-libs/scssphp.git",
"reference": "dde81c0a39d02e8e6fc81b70269747734e16d526"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scssphp/scssphp/zipball/63d1157457e5554edf00b0c1fabab4c1511d2520",
"reference": "63d1157457e5554edf00b0c1fabab4c1511d2520",
"url": "https://api.github.com/repos/combodo-itop-libs/scssphp/zipball/dde81c0a39d02e8e6fc81b70269747734e16d526",
"reference": "dde81c0a39d02e8e6fc81b70269747734e16d526",
"shasum": ""
},
"require": {
@@ -1626,8 +1626,8 @@
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": false,
"forward-command": false
"forward-command": false,
"bin-links": false
}
},
"autoload": {
@@ -1635,7 +1635,11 @@
"ScssPhp\\ScssPhp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"autoload-dev": {
"psr-4": {
"ScssPhp\\ScssPhp\\Tests\\": "tests/"
}
},
"license": [
"MIT"
],
@@ -1661,10 +1665,9 @@
"stylesheet"
],
"support": {
"issues": "https://github.com/scssphp/scssphp/issues",
"source": "https://github.com/scssphp/scssphp/tree/v1.13.0"
"source": "https://github.com/combodo-itop-libs/scssphp/tree/combodo/1.x"
},
"time": "2024-08-17T21:02:11+00:00"
"time": "2026-03-23T15:26:59+00:00"
},
{
"name": "soundasleep/html2text",
@@ -5097,7 +5100,8 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"apereo/phpcas": 20
"apereo/phpcas": 20,
"scssphp/scssphp": 20
},
"prefer-stable": false,
"prefer-lowest": false,

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

@@ -4344,7 +4344,9 @@ class AttributeText extends AttributeString
} else {
$sValue = self::RenderWikiHtml($sValue, true /* wiki only */);
return "<div class=\"HTML ibo-is-html-content\" $sStyle>".InlineImage::FixUrls($sValue).'</div>';
$sImageHtml = UserRights::IsLoggedIn() ? InlineImage::FixUrls($sValue) : InlineImage::ReplaceInlineImagesWithBase64Representation($sValue);
return "<div class=\"HTML ibo-is-html-content\" $sStyle>".$sImageHtml.'</div>';
}
}
@@ -8988,7 +8990,10 @@ class AttributeStopWatch extends AttributeDefinition
switch ($sThresholdCode) {
case 'deadline':
if ($value) {
if (is_int($value)) {
if (is_numeric($value)) {
if (!is_int($value)) {
$value = intval($value);
}
$sDate = date(AttributeDateTime::GetInternalFormat(), $value);
$sRet = AttributeDeadline::FormatDeadline($sDate);
} else {

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,6 +1746,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.force_login_when_no_delegated_authentication_endpoints_list' => [
'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,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'behind_reverse_proxy' => [
'type' => 'bool',
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',

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

@@ -296,6 +296,46 @@ class InlineImage extends DBObject
return $sHtml;
}
/**
* Replace <img> tags with a data-img-id attribute by the actual image in base64 representation
* so that the image can be displayed even if the download URL is not accessible (e.g. in unauthenticated approval templates)
*
* @param string $sHtml The HTML fragment to process
*
* @return String The modified HTML
* @since 3.2.3
*/
public static function ReplaceInlineImagesWithBase64Representation(string $sHtml): String
{
return preg_replace_callback(
'/<img\s+[^>]*'.static::DOM_ATTR_ID.'="(\d+)"[^>]*>/i',
function ($matches) {
// Extract inline image ID from the tag
$id = $matches[1];
try {
// Retrieve inline image
$oInline = MetaModel::GetObject(InlineImage::class, $id, true, true);
$oOrmDocument = $oInline->Get('contents');
// Replace src image by the base64 representation
$sInlineImageAsBase64 = base64_encode($oOrmDocument->GetData());
$sDataUri = 'data:'.$oOrmDocument->GetMimeType().';base64,'.$sInlineImageAsBase64;
$sImage = preg_replace('/src=["\'][^"\']+["\']/', 'src="'.$sDataUri.'"', $matches[0]);
// Remove sensitive information (the image ID and secret) from the tag
$sImage = preg_replace('/'.static::DOM_ATTR_ID.'="\d+"\s+'.static::DOM_ATTR_SECRET.'="\w+"/', '', $sImage);
} catch (Exception $e) {
$sImage = '<img src="" alt="'.Dict::S('UI:MissingInlineImage').'">';
}
return $sImage;
},
$sHtml
);
}
/**
* Add an extra attribute data-img-id for images which are based on an actual InlineImage
* so that we can later reconstruct the full "src" URL when needed

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");
}
}

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

@@ -41,6 +41,11 @@ SetupWebPage::AddModule(
'doc.manual_setup' => '',
'doc.more_information' => '',
// Security
'delegated_authentication_endpoints' => [
'ajax.backup.php',
],
// Default settings
//
'settings' => [

View File

@@ -242,8 +242,8 @@ try {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
// First step: prepare the datamodel, if it fails, roll-back
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', [], false, utils::ENUM_SANITIZATION_FILTER_MODULE_CODE);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', [], false, utils::ENUM_SANITIZATION_FILTER_MODULE_CODE);
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);

View File

@@ -37,6 +37,10 @@ SetupWebPage::AddModule(
// add your sample data XML files here,
],
'delegated_authentication_endpoints' => [
'ajax.php',
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any

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

@@ -317,7 +317,7 @@ class BrowseBrickHelper
$aRow[$key] = [
'level_alias' => $key,
'id' => $sCurrentObjectId,
'name' => utils::EscapeHtml($value->Get($sNameAttCode)),
'name' => $value->Get($sNameAttCode),
'class' => $sCurrentObjectClass,
'action_rules_token' => $this->PrepareActionRulesForItems($aItems, $key, $aLevelsProperties),
'metadata' => [
@@ -476,7 +476,7 @@ class BrowseBrickHelper
$aItems[$sCurrentIndex] = [
'level_alias' => $aCurrentRowKeys[0],
'id' => $aCurrentRowValues[0]->GetKey(),
'name' => utils::EscapeHtml($aCurrentRowValues[0]->Get($aLevelsProperties[$aCurrentRowKeys[0]]['name_att'])),
'name' => $aCurrentRowValues[0]->Get($aLevelsProperties[$aCurrentRowKeys[0]]['name_att']),
'class' => get_class($aCurrentRowValues[0]),
'subitems' => [],
'filter_data' => $this->GetFilterData($aLevelsProperties[$aCurrentRowKeys[0]], $aCurrentRowKeys[0], $aCurrentRowValues[0]),

View File

@@ -80,11 +80,11 @@
// N°4662 - Surround tooltip with div to ensure text retrival
if( (data.tooltip !== undefined) && ($('<div>'+data.tooltip+'</div>').text() !== ''))
{
cellElem.html( $('<span></span>').attr('data-tooltip-content', data.tooltip).attr('data-tooltip-html-enabled', true).html(data.name).prop('outerHTML') );
cellElem.html( $('<span></span>').attr('data-tooltip-content', data.tooltip).attr('data-tooltip-html-enabled', true).text(data.name).prop('outerHTML') );
}
else
{
cellElem.html(data.name);
cellElem.text(data.name);
}
// Building actions

View File

@@ -197,7 +197,7 @@
if( (item.name !== undefined) && (item.name !== '') )
{
iItemFlags += 1;
textWrapperElem.append( $('<div></div>').addClass('mosaic-item-name').html(item.name) );
textWrapperElem.append( $('<div></div>').addClass('mosaic-item-name').text(item.name) );
}
// - Adding description
if( (item.description !== undefined) && (item.description !== '') )

View File

@@ -233,7 +233,9 @@
{
case '{{ constant('Combodo\\iTop\\Portal\\Brick\\BrowseBrick::ENUM_ACTION_DRILLDOWN') }}':
spanElem.addClass('tree-toggle');
nameElem.html('<span class="glyphicon '+sNodeCollapsedClass+'" aria-hidden="true"></span><span class="list-group-item-text">'+nameElem.text()+'</span>');
var iconElem = $('<span></span>').addClass('glyphicon '+sNodeCollapsedClass).attr('aria-hidden', 'true');
var textElem = $('<span></span>').addClass('list-group-item-text').text(nameElem.text());
nameElem.empty().append(iconElem).append(textElem);
break;
case '{{ constant('Combodo\\iTop\\Portal\\Brick\\BrowseBrick::ENUM_ACTION_VIEW') }}':
url = '{{ app.url_generator.generate('p_object_view', {'sObjectClass': '-objectClass-', 'sObjectId': '-objectId-'})|raw }}'.replace(/-objectClass-/, item.class).replace(/-objectId-/, item.id);

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

@@ -1394,6 +1394,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:SelectInlineImageToUpload' => 'Vyberte obrázek',
'UI:AvailableInlineImagesLegend' => 'Dostupné obrázky',
'UI:NoInlineImage' => 'Na serveru není dostupný žádný obrázek. Nahrajte nějaký pomocí tlačítka výše.',
'UI:MissingInlineImage' => 'Chybějící obrázek',
'UI:ToggleFullScreen' => 'Přepnout zobrazení',
'UI:Button:ResetImage' => 'Obnovit původní obrázek',
'UI:Button:RemoveImage' => 'Odebrat obrázek',

View File

@@ -1397,6 +1397,7 @@ Ved tilknytningen til en trigger, bliver hver handling tildelt et "rækkefølge"
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Manglende billede',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1394,6 +1394,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
'UI:SelectInlineImageToUpload' => 'Wähle das Bild für den Upload aus',
'UI:AvailableInlineImagesLegend' => 'Verfügbare Bilder',
'UI:NoInlineImage' => 'Es sind keine Bilder auf dem Server verfügbar. Nutze den "Durchsuchen" Button oben, um ein Bild vom Computer hochzuladen.',
'UI:MissingInlineImage' => 'Bild fehlt',
'UI:ToggleFullScreen' => 'Maximieren / Minimieren',
'UI:Button:ResetImage' => 'Vorheriges Bild wiederherstellen',
'UI:Button:RemoveImage' => 'Bild löschen',

View File

@@ -1471,6 +1471,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:SelectInlineImageToUpload' => 'Select the image to upload',
'UI:AvailableInlineImagesLegend' => 'Available images',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.',
'UI:MissingInlineImage' => 'Missing image',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize',
'UI:Button:ResetImage' => 'Recover the previous image',

View File

@@ -1471,6 +1471,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:SelectInlineImageToUpload' => 'Select the image to upload',
'UI:AvailableInlineImagesLegend' => 'Available images',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.',
'UI:MissingInlineImage' => 'Missing image',
'UI:ToggleFullScreen' => 'Toggle Maximise / Minimise',
'UI:Button:ResetImage' => 'Recover the previous image',

View File

@@ -1397,6 +1397,7 @@ Cuando se asocien con un disparador, cada acción recibe un número de "orden",
'UI:SelectInlineImageToUpload' => 'Seleccione la imágen a subir',
'UI:AvailableInlineImagesLegend' => 'Imágenes disponibles',
'UI:NoInlineImage' => 'No hay imágenes disponibles en el servidor. Use el botón "Seleccionar archivo" para seleccionar una imágen de su equipo local y subirla al servidor.',
'UI:MissingInlineImage' => 'Imagen faltante',
'UI:ToggleFullScreen' => 'Cambiar Maximizar / Minimizar',
'UI:Button:ResetImage' => 'Recuperar imágen previa',
'UI:Button:RemoveImage' => 'Remover imágen',

View File

@@ -1398,6 +1398,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:SelectInlineImageToUpload' => 'Sélectionnez l\'image à ajouter',
'UI:AvailableInlineImagesLegend' => 'Images disponibles',
'UI:NoInlineImage' => 'Il n\'y a aucune image de disponible sur le serveur. Utilisez le bouton "Parcourir" (ci-dessus) pour sélectionner une image sur votre ordinateur et la télécharger sur le serveur.',
'UI:MissingInlineImage' => 'Image introuvable',
'UI:ToggleFullScreen' => 'Agrandir / Minimiser',
'UI:Button:ResetImage' => 'Récupérer l\'image initiale',
'UI:Button:RemoveImage' => 'Supprimer l\'image',

View File

@@ -1400,6 +1400,7 @@ A művelet eseményindítóhoz rendelésekor kap egy sorszámot , amely meghatá
'UI:SelectInlineImageToUpload' => 'Válasszon egy képet',
'UI:AvailableInlineImagesLegend' => 'Elérhető képek',
'UI:NoInlineImage' => 'A szerveren nincs elérhető kép. Használja a fenti "Tallózás" gombot egy kép kiválasztásához a számítógépéről, és töltse fel a szerverre.',
'UI:MissingInlineImage' => 'Hiányzó kép',
'UI:ToggleFullScreen' => 'Maximalizálás / Minimalizálás',
'UI:Button:ResetImage' => 'Az előző kép visszaállítása',
'UI:Button:RemoveImage' => 'Kép eltávolítása',

View File

@@ -1399,6 +1399,7 @@ Quando è associata a un trigger, a ogni azione è assegnato un numero "ordine",
'UI:SelectInlineImageToUpload' => 'Seleziona l\'immagine da caricare',
'UI:AvailableInlineImagesLegend' => 'Immagini disponibili',
'UI:NoInlineImage' => 'Non ci sono immagini disponibili sul server. Utilizza il pulsante "Sfoglia" sopra per selezionare un\'immagine dal tuo computer e caricarla sul server.',
'UI:MissingInlineImage' => 'Immagine mancante',
'UI:ToggleFullScreen' => 'Attiva/Disattiva a schermo intero',
'UI:Button:ResetImage' => 'Ripristina l\'immagine precedente',
'UI:Button:RemoveImage' => 'Rimuovi l\'immagine',

View File

@@ -1401,6 +1401,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1400,6 +1400,7 @@ Bij die koppeling wordt aan elke actie een volgorde-nummer gegeven. Dit bepaalt
'UI:SelectInlineImageToUpload' => 'Selecteer een afbeelding om te uploaden',
'UI:AvailableInlineImagesLegend' => 'Beschikbare afbeeldingen',
'UI:NoInlineImage' => 'Er is geen afbeelding beschikbaar op de server. Gebruik de "Afbeeldingen doorbladeren..." knop hierboven om een afbeelding te kiezen op je toestel.',
'UI:MissingInlineImage' => 'Ontbrekende afbeelding',
'UI:ToggleFullScreen' => 'Minimaliseren / Maximaliseren',
'UI:Button:ResetImage' => 'Vorige afbeelding herstellen',
'UI:Button:RemoveImage' => 'Afbeelding verwijderen',

View File

@@ -1408,6 +1408,7 @@ W przypadku powiązania z wyzwalaczem, każde działanie otrzymuje numer "porzą
'UI:SelectInlineImageToUpload' => 'Wybierz obraz do przesłania',
'UI:AvailableInlineImagesLegend' => 'Dostępne obrazy',
'UI:NoInlineImage' => 'Na serwerze nie ma obrazu. Użyj przycisku "Przeglądaj" powyżej, aby wybrać obraz ze swojego komputera i przesłać go na serwer.',
'UI:MissingInlineImage' => 'Brakujący obraz',
'UI:ToggleFullScreen' => 'Przełącz Maksymalizuj / Minimalizuj',
'UI:Button:ResetImage' => 'Odzyskaj poprzedni obraz',
'UI:Button:RemoveImage' => 'Usuń obraz',

View File

@@ -1393,6 +1393,7 @@ Quando associada a um gatilho, cada ação recebe um número de "ordem", especif
'UI:SelectInlineImageToUpload' => 'Selecione a imagem para enviar',
'UI:AvailableInlineImagesLegend' => 'Imagens disponíveis',
'UI:NoInlineImage' => 'Não há imagem disponível no servidor. Use o botão "Escolher arquivo" acima para selecionar uma imagem do seu computador e fazer o upload para o servidor',
'UI:MissingInlineImage' => 'Imagem ausente',
'UI:ToggleFullScreen' => 'Alternancia Maximizar / Minimizar',
'UI:Button:ResetImage' => 'Recupere a imagem anterior',
'UI:Button:RemoveImage' => 'Remover a imagem',

View File

@@ -1397,6 +1397,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:SelectInlineImageToUpload' => 'Выберите изображение для загрузки',
'UI:AvailableInlineImagesLegend' => 'Доступные изображения',
'UI:NoInlineImage' => 'На сервере нет доступных изображений. С помощью кнопки "Обзор..." выше выберите изображение на вашем компьютере, чтобы загрузить его на сервер.',
'UI:MissingInlineImage' => 'Отсутствует изображение',
'UI:ToggleFullScreen' => 'Развернуть / Свернуть',
'UI:Button:ResetImage' => 'Восстановить предыдущее изображение',
'UI:Button:RemoveImage' => 'Удалить изображение',

View File

@@ -1398,6 +1398,7 @@ Keď sú priradené spúštačom, každej akcii je dané číslo "príkazu", šp
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1401,6 +1401,7 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe
'UI:SelectInlineImageToUpload' => 'Select the image to upload~~',
'UI:AvailableInlineImagesLegend' => 'Available images~~',
'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.~~',
'UI:MissingInlineImage' => 'Missing image~~',
'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize~~',
'UI:Button:ResetImage' => 'Recover the previous image~~',
'UI:Button:RemoveImage' => 'Remove the image~~',

View File

@@ -1398,6 +1398,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:SelectInlineImageToUpload' => '选择要上传的图片',
'UI:AvailableInlineImagesLegend' => '可用的图片',
'UI:NoInlineImage' => '服务器上没有图片. 使用上面的 "浏览" 按钮, 从您的电脑上选择并上传到服务器.',
'UI:MissingInlineImage' => '缺少图片',
'UI:ToggleFullScreen' => '切换最大化/最小化',
'UI:Button:ResetImage' => '恢复之前的图片',
'UI:Button:RemoveImage' => '移除图片',

View File

@@ -301,88 +301,112 @@ function ValidateField(sFieldId, sPattern, bMandatory, sFormId, nullValue, origi
return true; // Do not stop propagation ??
}
function ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue)
function EvaluateCKEditorValidation(oOptions)
{
let oField = $('#'+sFieldId);
let oField = $('#'+oOptions.sFieldId);
if (oField.length === 0) {
return false;
}
let oCKEditor = CombodoCKEditorHandler.GetInstanceSynchronous('#'+sFieldId);
let oCKEditor = CombodoCKEditorHandler.GetInstanceSynchronous('#'+oOptions.sFieldId);
let bValid = true;
let sExplain = '';
let sTextContent;
let sTextOriginalContents;
var bValid;
var sExplain = '';
if (oField.prop('disabled')) {
bValid = true; // disabled fields are not checked
} else {
// If the CKEditor is not yet loaded, we need to wait for it to be ready
// but as we need this function to be synchronous, we need to call it again when the CKEditor is ready
if (oCKEditor === undefined){
CombodoCKEditorHandler.GetInstance('#'+sFieldId).then((oCKEditor) => {
ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue);
CombodoCKEditorHandler.GetInstance('#'+oOptions.sFieldId).then((oCKEditor) => {
oOptions.onRetry();
});
return;
return false;
}
let sTextContent;
let sFormattedContent = oCKEditor.getData();
// Get the contents without the tags
// Check if we have a formatted content that is HTML, otherwise we just have plain text, and we can use it directly
let sFormattedContent = oCKEditor.getData();
sTextContent = $(sFormattedContent).length > 0 ? $(sFormattedContent).text() : sFormattedContent;
if (sTextContent === '') {
if (sTextContent === '')
{
// No plain text, maybe there is just an image
let oImg = $(sFormattedContent).find("img");
if (oImg.length !== 0) {
let oImg = $(sFormattedContent).find('img');
if (oImg.length !== 0)
{
sTextContent = 'image';
}
}
// Get the original value without the tags
let oFormattedOriginalContents = (originalValue !== undefined) ? $('<div></div>').html(originalValue) : undefined;
let sTextOriginalContents = (oFormattedOriginalContents !== undefined) ? oFormattedOriginalContents.text() : undefined;
let oFormattedOriginalContents = (oOptions.sOriginalValue !== undefined) ? $('<div></div>').html(oOptions.sOriginalValue) : undefined;
sTextOriginalContents = (oFormattedOriginalContents !== undefined) ? oFormattedOriginalContents.text() : undefined;
if (bMandatory && (sTextContent === nullValue)) {
bValid = false;
sExplain = Dict.S('UI:ValueMustBeSet');
} else if ((sTextOriginalContents !== undefined) && (sTextContent === sTextOriginalContents)) {
bValid = false;
if (sTextOriginalContents === nullValue) {
sExplain = Dict.S('UI:ValueMustBeSet');
} else {
// Note: value change check is not working well yet as the HTML to Text conversion is not exactly the same when done from the PHP value or the CKEditor value.
sExplain = Dict.S('UI:ValueMustBeChanged');
}
} else {
bValid = true;
if (oOptions.validate !== undefined) {
let oValidation = oOptions.validate(sTextContent, sTextOriginalContents);
bValid = oValidation.bValid;
sExplain = oValidation.sExplain;
}
// Put and event to check the field when the content changes, remove the event right after as we'll call this same function again, and we don't want to call the event more than once (especially not ^2 times on each call)
// Put an event to check the field when the content changes, remove the event right after as we'll call this same function again, and we don't want to call the event more than once (especially not ^2 times on each call)
oCKEditor.model.document.once('change:data', (event) => {
ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue);
oOptions.onChange();
});
}
ReportFieldValidationStatus(sFieldId, sFormId, bValid, sExplain);
ReportFieldValidationStatus(oOptions.sFieldId, oOptions.sFormId, bValid, sExplain);
return bValid;
}
function ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue)
{
return EvaluateCKEditorValidation({
sFieldId: sFieldId,
sFormId: sFormId,
sOriginalValue: originalValue,
onRetry: function() {
ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue);
},
onChange: function() {
ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue, originalValue);
},
validate: function(sTextContent, sTextOriginalContents) {
var bValid;
var sExplain = '';
if (bMandatory && (sTextContent === nullValue)) {
bValid = false;
sExplain = Dict.S('UI:ValueMustBeSet');
} else if ((sTextOriginalContents !== undefined) && (sTextContent === sTextOriginalContents)) {
bValid = false;
if (sTextOriginalContents === nullValue) {
sExplain = Dict.S('UI:ValueMustBeSet');
} else {
// Note: value change check is not working well yet as the HTML to Text conversion is not exactly the same when done from the PHP value or the CKEditor value.
sExplain = Dict.S('UI:ValueMustBeChanged');
}
} else {
bValid = true;
}
return {bValid: bValid, sExplain: sExplain};
}
});
}
function ResetPwd(id)
{
// Reset the values of the password fields
$('#'+id).val('*****');
$('#'+id+'_confirm').val('*****');
// And reset the flag, to tell it that the password remains unchanged
$('#'+id+'_changed').val(0);
// Visual feedback, None when it's Ok
$('#v_'+id).html('');
// Reset the values of the password fields
$('#'+id).val('*****');
$('#'+id+'_confirm').val('*****');
// And reset the flag, to tell it that the password remains unchanged
$('#'+id+'_changed').val(0);
// Visual feedback, None when it's Ok
$('#v_'+id).html('');
}
// Called whenever the content of a one way encrypted password changes
function PasswordFieldChanged(id)
{
// Set the flag, to tell that the password changed
$('#'+id+'_changed').val(1);
// Set the flag, to tell that the password changed
$('#'+id+'_changed').val(1);
}
// Special validation function for one way encrypted password fields
@@ -415,37 +439,48 @@ function ValidatePasswordField(id, sFormId)
// to determine if the field is empty or not
function ValidateCaseLogField(sFieldId, bMandatory, sFormId, nullValue, originalValue)
{
var bValid = true;
var sExplain = '';
var sTextContent;
if ($('#'+sFieldId).prop('disabled'))
{
bValid = true; // disabled fields are not checked
}
else
{
// Get the contents (with tags)
// Note: For CaseLog we can't retrieve the formatted contents from CKEditor (unlike in ValidateCKEditorField() method) because of the place holder.
sTextContent = $('#' + sFieldId).val();
var count = $('#'+sFieldId+'_count').val();
return EvaluateCKEditorValidation({
sFieldId: sFieldId,
sFormId: sFormId,
sOriginalValue: originalValue,
onRetry: function() {
ValidateCaseLogField(sFieldId, bMandatory, sFormId, nullValue, originalValue);
},
onChange: function() {
ValidateCaseLogField(sFieldId, bMandatory, sFormId, nullValue, originalValue);
},
validate: function(sTextContent, sTextOriginalContents) {
var bValid;
var sExplain = '';
// CaseLog is special: history count matters when deciding if the field is empty
var count = $('#'+sFieldId+'_count').val();
if (bMandatory && (count == 0) && (sTextContent == nullValue))
{
// No previous entry and no content typed
bValid = false;
sExplain = Dict.S('UI:ValueMustBeSet');
if (bMandatory && (count == 0) && (sTextContent === nullValue))
{
// No previous entry and no content typed
bValid = false;
sExplain = Dict.S('UI:ValueMustBeSet');
}
else if ((sTextOriginalContents !== undefined) && (sTextContent === sTextOriginalContents))
{
bValid = false;
if (sTextOriginalContents === nullValue)
{
sExplain = Dict.S('UI:ValueMustBeSet');
}
else
{
// Note: value change check is not working well yet as the HTML to Text conversion is not exactly the same when done from the PHP value or the CKEditor value.
sExplain = Dict.S('UI:ValueMustBeChanged');
}
}
else
{
bValid = true;
}
return {bValid: bValid, sExplain: sExplain};
}
else if ((originalValue != undefined) && (sTextContent == originalValue))
{
bValid = false;
sExplain = Dict.S('UI:ValueMustBeChanged');
}
}
ReportFieldValidationStatus(sFieldId, sFormId, bValid, '' /* sExplain */);
// We need to check periodically as CKEditor doesn't trigger our events. More details in UIHTMLEditorWidget::Display() @ line 92
setTimeout(function(){ValidateCaseLogField(sFieldId, bMandatory, sFormId, nullValue, originalValue);}, 500);
});
}
// Validate the inputs depending on the current setting

View File

@@ -332,6 +332,12 @@ CombodoModal._ConvertButtonDefinition = function (aButtonsDefinitions) {
class: typeof(element.classes) !== 'undefined' ? element.classes.join(' ') : '',
click: element.callback_on_click
}
// id is optional, and we don't want to set it if not defined
if (typeof element.id !== 'undefined' && element.id !== null) {
aButton.id = element.id;
}
aConverted.push(aButton);
}
);

View File

@@ -35,116 +35,63 @@ function SearchFormForeignKeys(id, sTargetClass, sAttCode, oSearchWidgetElmt, sF
this.sAttCode = sAttCode;
this.oSearchWidgetElmt = oSearchWidgetElmt;
this.emptyHtml = ''; // content to be displayed when the search results are empty (when opening the dialog)
this.emptyOnClose = true; // Workaround for the JQuery dialog being very slow when opening and closing if the content contains many INPUT tags
this.ajax_request = null;
// this.bSelectMode = bSelectMode; // true if the edited field is a SELECT, false if it's an autocomplete
// this.bSearchMode = bSearchMode; // true if selecting a value in the context of a search form
var me = this;
this.Init = function()
{
// make sure that the form is clean
$('#linkedset_'+this.id+' .selection').each( function() { this.checked = false; });
$('#'+this.id+'_btnRemove').prop('disabled', false);
$('<div id="dlg_'+me.id+'"></div>').appendTo(document.body);
// me.trace(dialog);
//TODO : check and remove all unneded code bellow this line!!
$('#'+this.id+'_linksToRemove').val('');
$('#linkedset_'+me.id).on('remove', function() {
// prevent having the dlg div twice
$('#dlg_'+me.id).remove();
});
$('#'+this.iInputId).closest('form').on('submit', function() {
return me.OnFormSubmit();
});
};
this.StopPendingRequest = function()
{
if (me.ajax_request)
{
me.ajax_request.abort();
me.ajax_request = null;
}
};
this.ShowModalSearchForeignKeys = function()
{
// // Query the server to get the form to search for target objects
// if (me.bSelectMode)
// {
// $('#fstatus_'+me.id).html('<img src="../images/indicator.gif" />');
// }
// else
// {
// $('#label_'+me.id).addClass('dlg_loading');
// }
$('#label_'+me.id).addClass('dlg_loading');
var theMap = {
sAttCode: me.sAttCode,
iInputId: me.id,
sTitle: me.sTitle,
sTargetClass: me.sTargetClass,
// bSearchMode: me.bSearchMode,
operation: 'ShowModalSearchForeignKeys'
};
const oModalParams = {
content: {
endpoint: AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'),
data: {
sAttCode: me.sAttCode,
iInputId: me.id,
sTargetClass: me.sTargetClass,
operation: 'ShowModalSearchForeignKeys'
},
},
title: me.sTitle,
id: 'dlg_'+me.id,
size: 'lg',
buttons: [
{
text: Dict.S('UI:Button:Cancel'),
callback_on_click: function() {
$(this).dialog("close");
},
classes: ['cancel', 'ibo-is-alternative', 'ibo-is-neutral'],
},
{
text: Dict.S('UI:Button:Add'),
id: "btn_ok_"+me.id,
classes: ['ok', 'ibo-is-regular', 'ibo-is-primary'],
callback_on_click: function() {
me.DoAddObjects();
}
}
],
callback_on_content_loaded: function(oModalContentElement){
// Update initial buttons state
me.UpdateButtons();
},
extra_options: {
callback_on_modal_close: function () {
$(this).remove(); // destroy then remove dialog object
}
}
}
const oModal = CombodoModal.OpenModal(oModalParams);
// Make sure that we cancel any pending request before issuing another
// since responses may arrive in arbitrary order
me.StopPendingRequest();
// Run the query and get the result back directly in HTML
me.ajax_request = $.post( AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
function(data)
{
// $('#dlg_'+me.id).html(data);
$('#dlg_'+me.id).empty().append($(data)); // $(data).filter(':not(script)'));
$('#dlg_'+me.id).dialog('open');
me.UpdateSizes();
me.UpdateButtons();
me.ajax_request = null;
me.ListResultsSearchForeignKeys();
},
'html'
);
};
this.UpdateSizes = function()
{
var dlg = $('#dlg_'+me.id);
// Adjust the dialog's size to fit into the screen
if (dlg.width() > ($(window).width()-40))
{
dlg.width($(window).width()-40);
}
if (dlg.height() > ($(window).height()-70))
{
dlg.height($(window).height()-70);
}
var searchForm = dlg.find('div.display_block:first'); // Top search form, enclosing display_block
var results = $('#SearchResultsToAdd_'+me.id);
var oPadding = {};
var aKeys = ['top', 'right', 'bottom', 'left'];
for(k in aKeys)
{
oPadding[aKeys[k]] = 0;
if (dlg.css('padding-'+aKeys[k]))
{
oPadding[aKeys[k]] = parseInt(dlg.css('padding-'+aKeys[k]).replace('px', ''));
}
}
//var width = dlg.innerWidth() - oPadding['right'] - oPadding['left'] - 22; // 5 (margin-left) + 5 (padding-left) + 5 (padding-right) + 5 (margin-right) + 2 for rounding !
var height = dlg.innerHeight()-oPadding['top']-oPadding['bottom']-22;
var form_height = searchForm.outerHeight();
results.height(height - form_height - 40); // Leave some space for the buttons
// Bind events
oModal.on('change', '#count_'+me.id, function(){
me.UpdateButtons();
});
};
this.UpdateButtons = function()
@@ -160,63 +107,6 @@ function SearchFormForeignKeys(id, sTargetClass, sAttCode, oSearchWidgetElmt, sF
}
};
/**
* @return {boolean}
*/
this.ListResultsSearchForeignKeys = function ()
{
var theMap = {
sTargetClass: me.sTargetClass,
iInputId: me.id,
sFilter: me.sfilter,
// bSearchMode: me.bSearchMode
};
// Gather the parameters from the search form
$('#fs_'+me.id+' :input').each( function() {
if (this.name !== '')
{
var val = $(this).val(); // supports multiselect as well
if (val !== null)
{
theMap[this.name] = val;
}
}
});
theMap['sRemoteClass'] = theMap['class']; // swap 'class' (defined in the form) and 'remoteClass'
theMap.operation = 'ListResultsSearchForeignKeys'; // Override what is defined in the form itself
theMap.sAttCode = me.sAttCode;
var sSearchAreaId = '#SearchResultsToAdd_'+me.id;
//$(sSearchAreaId).html('<div style="text-align:center;width:100%;height:24px;vertical-align:middle;"><img src="../images/indicator.gif" /></div>');
$(sSearchAreaId).block();
me.UpdateButtons();
// Make sure that we cancel any pending request before issuing another
// since responses may arrive in arbitrary order
me.StopPendingRequest();
// Run the query and display the results
me.ajax_request = $.post(AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
function(data)
{
$(sSearchAreaId).html(data);
$('#fr_'+me.id+' input:radio').on('click', function() { me.UpdateButtons(); });
me.UpdateButtons();
me.ajax_request = null;
$('#count_'+me.id).on('change', function(){
me.UpdateButtons();
});
me.UpdateSizes();
},
'html'
);
return false; // Don't submit the form, stay in the current page !
};
/**
* @return {boolean}
*/
@@ -286,56 +176,4 @@ function SearchFormForeignKeys(id, sTargetClass, sAttCode, oSearchWidgetElmt, sF
return false;
};
// Workaround for a ui.jquery limitation: if the content of
// the dialog contains many INPUTs, closing and opening the
// dialog is very slow. So empty it each time.
this.OnClose = function()
{
me.StopPendingRequest();
// called by the dialog, so in the context 'this' points to the jQueryObject
if (me.emptyOnClose)
{
$('#SearchResultsToAdd_'+me.id).html(me.emptyHtml);
}
$('#label_'+me.id).removeClass('dlg_loading');
$('#label_'+me.id).focus();
me.ajax_request = null;
};
this.DoSelectObjectClass = function()
{
// Retrieving selected value
var oSelectedClass = $('#ac_create_'+me.id+' select');
if(oSelectedClass.length !== 1) return;
// Setting new target class
me.sTargetClass = oSelectedClass.val();
// Opening real creation form
$('#ac_create_'+me.id).dialog('close');
me.CreateObject();
};
this.Update = function()
{
if ($('#'+me.id).prop('disabled'))
{
$('#v_'+me.id).html('');
$('#label_'+me.id).prop('disabled', true);
$('#label_'+me.id).css({'background': 'transparent'});
$('#mini_add_'+me.id).hide();
$('#mini_tree_'+me.id).hide();
$('#mini_search_'+me.id).hide();
}
else
{
$('#label_'+me.id).prop('disabled', false);
$('#label_'+me.id).css({'background': '#fff url(../images/ac-background.gif) no-repeat right'});
$('#mini_add_'+me.id).show();
$('#mini_tree_'+me.id).show();
$('#mini_search_'+me.id).show();
}
};
}

View File

@@ -1654,17 +1654,17 @@
},
{
"name": "scssphp/scssphp",
"version": "v1.13.0",
"version_normalized": "1.13.0.0",
"version": "dev-combodo/1.x",
"version_normalized": "dev-combodo/1.x",
"source": {
"type": "git",
"url": "https://github.com/scssphp/scssphp.git",
"reference": "63d1157457e5554edf00b0c1fabab4c1511d2520"
"url": "https://github.com/combodo-itop-libs/scssphp.git",
"reference": "dde81c0a39d02e8e6fc81b70269747734e16d526"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scssphp/scssphp/zipball/63d1157457e5554edf00b0c1fabab4c1511d2520",
"reference": "63d1157457e5554edf00b0c1fabab4c1511d2520",
"url": "https://api.github.com/repos/combodo-itop-libs/scssphp/zipball/dde81c0a39d02e8e6fc81b70269747734e16d526",
"reference": "dde81c0a39d02e8e6fc81b70269747734e16d526",
"shasum": ""
},
"require": {
@@ -1687,15 +1687,15 @@
"ext-iconv": "Can be used as fallback when ext-mbstring is not available",
"ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv"
},
"time": "2024-08-17T21:02:11+00:00",
"time": "2026-03-23T15:26:59+00:00",
"bin": [
"bin/pscss"
],
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": false,
"forward-command": false
"forward-command": false,
"bin-links": false
}
},
"installation-source": "dist",
@@ -1704,7 +1704,11 @@
"ScssPhp\\ScssPhp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"autoload-dev": {
"psr-4": {
"ScssPhp\\ScssPhp\\Tests\\": "tests/"
}
},
"license": [
"MIT"
],
@@ -1730,8 +1734,7 @@
"stylesheet"
],
"support": {
"issues": "https://github.com/scssphp/scssphp/issues",
"source": "https://github.com/scssphp/scssphp/tree/v1.13.0"
"source": "https://github.com/combodo-itop-libs/scssphp/tree/combodo/1.x"
},
"install-path": "../scssphp/scssphp"
},

View File

@@ -292,9 +292,9 @@
'dev_requirement' => false,
),
'scssphp/scssphp' => array(
'pretty_version' => 'v1.13.0',
'version' => '1.13.0.0',
'reference' => '63d1157457e5554edf00b0c1fabab4c1511d2520',
'pretty_version' => 'dev-combodo/1.x',
'version' => 'dev-combodo/1.x',
'reference' => 'dde81c0a39d02e8e6fc81b70269747734e16d526',
'type' => 'library',
'install_path' => __DIR__ . '/../scssphp/scssphp',
'aliases' => array(),

View File

@@ -5052,7 +5052,7 @@ EOL;
*
* @return array
*/
protected function multiplyMedia(Environment $env = null, $childQueries = null)
protected function multiplyMedia(?Environment $env = null, $childQueries = null)
{
if (
! isset($env) ||
@@ -5144,7 +5144,7 @@ EOL;
*
* @return \ScssPhp\ScssPhp\Compiler\Environment
*/
protected function pushEnv(Block $block = null)
protected function pushEnv(?Block $block = null)
{
$env = new Environment();
$env->parent = $this->env;
@@ -5208,7 +5208,7 @@ EOL;
*
* @return void
*/
protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
protected function set($name, $value, $shadow = false, ?Environment $env = null, $valueUnreduced = null)
{
$name = $this->normalizeName($name);
@@ -5314,7 +5314,7 @@ EOL;
*
* @return mixed|null
*/
public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
public function get($name, $shouldThrow = true, ?Environment $env = null, $unreduced = false)
{
$normalizedName = $this->normalizeName($name);
$specialContentKey = static::$namespaces['special'] . 'content';
@@ -5379,7 +5379,7 @@ EOL;
*
* @return bool
*/
protected function has($name, Environment $env = null)
protected function has($name, ?Environment $env = null)
{
return ! \is_null($this->get($name, false, $env));
}

View File

@@ -272,7 +272,7 @@ abstract class Formatter
*
* @return string
*/
public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null)
public function format(OutputBlock $block, ?SourceMapGenerator $sourceMapGenerator = null)
{
$this->sourceMapGenerator = null;

View File

@@ -578,7 +578,7 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
*
* @return string
*/
public function output(Compiler $compiler = null)
public function output(?Compiler $compiler = null)
{
$dimension = round($this->dimension, self::PRECISION);

View File

@@ -140,7 +140,7 @@ class Parser
* @param bool $cssOnly
* @param LoggerInterface|null $logger
*/
public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null)
public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', ?Cache $cache = null, $cssOnly = false, ?LoggerInterface $logger = null)
{
$this->sourceName = $sourceName ?: '(stdin)';
$this->sourceIndex = $sourceIndex;

View File

@@ -59,7 +59,7 @@ final class Warn
*
* @internal
*/
public static function setCallback(callable $callback = null)
public static function setCallback(?callable $callback = null)
{
$previousCallback = self::$callback;
self::$callback = $callback;

View File

@@ -1186,12 +1186,6 @@ try {
if ($bRes) {
try {
$bApplyStimulus = $oObj->ApplyStimulus($sStimulus); // will write the object in the DB
}
catch (CoreCannotSaveObjectException $e) {
// Rollback to the previous state... by reloading the object from the database and applying the modifications again
$oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey());
$oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes);
$sIssues = implode(' ', $e->getIssues());
} catch (CoreException $e) {
// Rollback to the previous state... by reloading the object from the database and applying the modifications again
$oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey());

View File

@@ -34,6 +34,7 @@ try {
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
IssueLog::Trace('----- Request: '.utils::GetRequestUri(), LogChannels::WEB_REQUEST);
$oPage = new DownloadPage("");
@@ -43,7 +44,7 @@ try {
switch ($operation) {
case 'download_document':
LoginWebPage::DoLoginEx('backoffice', false);
LoginWebPage::DoLoginEx();
$id = utils::ReadParam('id', '');
$sField = utils::ReadParam('field', '');
if ($sClass == 'Attachment') {
@@ -63,8 +64,7 @@ try {
break;
case 'download_inlineimage':
// No login is required because the "secret" protects us
// Benefit: the inline image can be inserted into any HTML (templating = $this->html(public_log)$)
LoginWebPage::DoLoginEx();
$id = utils::ReadParam('id', '');
$sSecret = utils::ReadParam('s', '');
$iCacheSec = 31556926; // One year ahead: an inline image cannot change

View File

@@ -173,10 +173,9 @@ try {
case 'ShowModalSearchForeignKeys':
$oPage->SetContentType('text/html');
$iInputId = utils::ReadParam('iInputId', '');
$sTitle = utils::ReadParam('sTitle', '', false, 'raw_data');
$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
$oWidget = new UISearchFormForeignKeys($sTargetClass, $iInputId);
$oWidget->ShowModalSearchForeignKeys($oPage, $sTitle);
$oWidget->ShowModalSearchForeignKeys($oPage);
break;
// ui.searchformforeignkeys
@@ -187,16 +186,6 @@ try {
$oWidget->GetFullListForeignKeysFromSelection($oPage, $oFullSetFilter);
break;
// ui.searchformforeignkeys
case 'ListResultsSearchForeignKeys':
$oPage->SetContentType('text/html');
$sTargetClass = utils::ReadParam('sTargetClass', '', false, 'class');
$iInputId = utils::ReadParam('iInputId', '');
$sRemoteClass = utils::ReadParam('sRemoteClass', '', false, 'class');
$oWidget = new UISearchFormForeignKeys($sTargetClass, $iInputId);
$oWidget->ListResultsSearchForeignKeys($oPage, $sRemoteClass);
break;
// ui.linkswidget
case 'addObjects':
$oPage->SetContentType('text/html');
@@ -2047,6 +2036,17 @@ EOF
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
// Check user has access to the object before trying to acquire the lock
$oSearch = new DBObjectSearch($sObjClass);
$oSearch->AddCondition(MetaModel::DBGetKey($sObjClass), $iObjKey, '=');
$oSet = new CMDBObjectSet($oSearch);
if (
false === $oSet->CountExceeds(0) ||
UserRights::IsActionAllowed($sObjClass, UR_ACTION_MODIFY, $oSet) !== UR_ALLOWED_YES
) {
throw new SecurityException(Dict::S('UI:ObjectDoesNotExist'));
}
$aResult = iTopOwnershipLock::AcquireLock($sObjClass, $iObjKey);
if (false === $aResult['success']) {
$aLockData = iTopOwnershipLock::IsLocked($sObjClass, $iObjKey);
@@ -2502,8 +2502,7 @@ EOF
$oKPI->ComputeAndReport('Data fetch and format');
$oPage->output();
} catch (Exception $e) {
// note: transform to cope with XSS attacks
echo utils::EscapeHtml($e->GetMessage());
echo utils::EscapeHtml(Dict::S('UI:PageTitle:FatalError'));
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
}

View File

@@ -87,7 +87,7 @@ if (is_link($sPageEnvFullPath)) {
}
$sTargetPage = CheckPageExists($sPageEnvFullPath, $aPossibleBasePaths);
if ($sTargetPage === false) {
if ($sTargetPage === false || $sModule === 'core' || $sModule === 'dictionaries') {
// Do not recall the page parameters (security takes precedence)
echo "Wrong module, page name or environment...";
exit;
@@ -97,4 +97,41 @@ if ($sTargetPage === false) {
//
// GO!
//
// check module white list
// check conf param
// 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');
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)) {
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');
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.");
}
require_once($sTargetPage);
function GetModuleDelegatedAuthenticationEndpoints(string $sModuleName): ?array
{
$sModuleFile = utils::GetAbsoluteModulePath($sModuleName).'/module.'.$sModuleName.'.php';
if (!file_exists($sModuleFile)) {
echo 'Wrong module, page name or environment...';
exit;
}
require_once APPROOT.'setup/extensionsmap.class.inc.php';
$oExtensionMap = new iTopExtensionsMap();
$aModuleParam = $oExtensionMap->GetModuleInfo($sModuleFile)[2];
return $aModuleParam['delegated_authentication_endpoints'] ?? null;
}

View File

@@ -390,7 +390,7 @@ class iTopExtensionsMap
* @param string $sModuleFile
* @return array
*/
protected function GetModuleInfo($sModuleFile)
public function GetModuleInfo($sModuleFile)
{
static $iDummyClassIndex = 0;

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,6 +36,8 @@ class SetupPage extends NiceWebPage
{
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/setuppage/layout';
protected const BODY_DATA_GUI_TYPE = 'setup';
public function __construct($sTitle)
{
parent::__construct($sTitle);

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

@@ -233,6 +233,7 @@ class AjaxPage extends WebPage implements iTabbedPage
'aJsInlineLive' => $this->a_scripts,
'aJsInlineOnDomReady' => $this->GetReadyScripts(),
'aJsInlineOnInit' => $this->a_init_scripts,
'sBodyDataGuiType' => static::BODY_DATA_GUI_TYPE,
'bEscapeContent' => ($this->sContentType == 'text/html') && ($this->sContentDisposition == 'inline'),
// TODO 3.0.0: TEMP, used while developping, remove it.
'sSanitizedContent' => utils::FilterXSS($this->s_content),

View File

@@ -172,6 +172,7 @@ class UnauthenticatedWebPage extends NiceWebPage
'aJsInlineLive' => $this->a_scripts,
'aJsInlineOnDomReady' => $this->GetReadyScripts(),
'aJsInlineOnInit' => $this->a_init_scripts,
'sBodyDataGuiType' => static::BODY_DATA_GUI_TYPE,
// TODO 3.0.0: TEMP, used while developing, remove it.
'sCapturedOutput' => utils::FilterXSS($s_captured_output),

View File

@@ -152,6 +152,8 @@ class WebPage implements Page
*/
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/webpage/layout';
protected const BODY_DATA_GUI_TYPE = 'backoffice';
protected $s_title;
protected $s_content;
protected $s_deferred_content;
@@ -1702,6 +1704,7 @@ JS;
'aJsInlineLive' => $this->a_scripts,
'aJsInlineOnDomReady' => $this->GetReadyScripts(),
'aJsInlineOnInit' => $this->a_init_scripts,
'sBodyDataGuiType' => static::BODY_DATA_GUI_TYPE,
// TODO 3.0.0: TEMP, used while developing, remove it.
'sCapturedOutput' => utils::FilterXSS($s_captured_output),

View File

@@ -1003,6 +1003,7 @@ HTML;
'aJsInlineOnInit' => $this->a_init_scripts,
'aJsInlineOnDomReady' => $this->GetReadyScripts(),
'aJsInlineLive' => $this->a_scripts,
'sBodyDataGuiType' => static::BODY_DATA_GUI_TYPE,
// TODO 3.0.0: TEMP, used while developping, remove it.
'sSanitizedContent' => utils::FilterXSS($this->s_content),
'sDeferredContent' => utils::FilterXSS($this->s_deferred_content),

View File

@@ -42,6 +42,7 @@ use RunTimeEnvironment;
use ScalarExpression;
use SetupUtils;
use UILinksWidget;
use UserRights;
use utils;
use WizardHelper;
@@ -71,6 +72,15 @@ class AjaxRenderController
$bShowObsoleteData = utils::ShowObsoleteData();
}
$oSet->SetShowObsoleteData($bShowObsoleteData);
// N°8606 : Check user permissions on the main class
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()));
}
$aResult["draw"] = $iDrawNumber;
$aResult["recordsTotal"] = $oSet->Count();
$aResult["recordsFiltered"] = $aResult["recordsTotal"] ;
@@ -95,6 +105,14 @@ class AjaxRenderController
continue;
}
// N°8606 : Check user permissions on the current class
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));
}
foreach ($aColumnsLoad[$sAlias] as $sAttCode) {
$aObj[$sAlias."/".$sAttCode] = $aObject[$sAlias]->GetAsHTML($sAttCode);
$bExcludeRawValue = false;

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

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

View File

@@ -1,6 +1,6 @@
[infra]
; STS version : testing greatest PHP version possible
php_version=8.3-apache
php_version=8.4-apache
; N°6629 perf bug on some tests on mariadb for now, so specifying MySQL
db_version=latest-mariadb

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Config;
use Exception;
use MetaModel;
class LoginWebPageTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
private Config $oConfig;
public const PASSWORD = 'a209320P!ù;ralùqpi,pàcqi"nr';
public function setUp(): void
{
parent::setUp();
$sConfigPath = MetaModel::GetConfig()->GetLoadedFile();
$this->oConfig = new Config($sConfigPath);
$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");
}
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");
}
mkdir($sFolderPath);
$this->RecurseCopy(__DIR__.'/extension-without-delegated-authentication-endpoints-list', $sFolderPath);
}
public function tearDown(): void
{
parent::tearDown();
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
$this->RecurseRmdir($sFolderPath);
} else {
throw new Exception("Folder $sFolderPath does not exist, it should have been created in setUp");
}
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
if (file_exists($sFolderPath)) {
$this->RecurseRmdir($sFolderPath);
} else {
throw new Exception("Folder $sFolderPath does not exist, it should have been created in setUp");
}
}
protected function GivenConfigFileAllowedLoginTypes($aAllowedLoginTypes): void
{
@chmod($this->oConfig->GetLoadedFile(), 0770);
$this->oConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
$this->oConfig->WriteToFile($this->oConfig->GetLoadedFile());
@chmod($this->oConfig->GetLoadedFile(), 0444);
}
/**
*
* @throws \Exception
*/
public function testInDelegatedAuthenticationEndpoints()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileInDelegatedAuthenticationEndpointsList.php",
[],
[],
true
);
$this->assertStringNotContainsString('<title>iTop login</title>', $sPageContent, 'File listed in delegated authentication endpoints list (in the module), login should not be requested by exec.');
}
public function testUserCanAccessAnyFile()
{
// generate random login
$sUserLogin = 'user-'.date('YmdHis');
$this->CreateUser($sUserLogin, self::$aURP_Profiles['Service Desk Agent'], self::PASSWORD);
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileNotInDelegatedAuthenticationEndpointsList.php",
[
'auth_user' => $sUserLogin,
'auth_pwd' => self::PASSWORD,
],
[],
true
);
$this->assertStringContainsString('Yo', $sPageContent, 'Logged in user should access any file via exec.php even if the page isn\'t listed in delegated authentication endpoints list');
}
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->WriteToFile();
@chmod($this->oConfig->GetLoadedFile(), 0444);
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-without-delegated-authentication-endpoints-list&exec_page=src/Controller/File.php",
);
$this->assertStringContainsString('<title>iTop login</title>', $sPageContent, 'if itop is configured to force login when no there is no delegated authentication endpoints list, then login should be required.');
}
public function testWithoutDelegatedAuthenticationEndpointsListWithDefaultConfiguration()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-without-delegated-authentication-endpoints-list&exec_page=src/Controller/File.php",
[],
[],
true
);
$this->assertStringContainsString('Yo', $sPageContent, 'by default (until N°9343) if no delegated authentication endpoints list is defined, not logged in persons should access pages');
}
public function testNotInDelegatedAuthenticationEndpointsList()
{
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileNotInDelegatedAuthenticationEndpointsList.php",
[],
[],
true
);
$this->assertStringContainsString('<title>iTop login</title>', $sPageContent, 'Since an delegated authentication endpoints list is defined and file isn\'t listed in it, login should be required');
}
/**
* @dataProvider InDelegatedAuthenticationEndpointsWithAdminRequiredProvider
*
* @throws \Exception
*/
public function testInDelegatedAuthenticationEndpointsWithAdminRequired($iProfileId, $bShouldSeeForbiddenAdminPage)
{
// generate random login
$sUserLogin = 'user-'.date('YmdHis');
$this->CreateUser($sUserLogin, $iProfileId, self::PASSWORD);
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
$sPageContent = $this->CallItopUri(
"pages/exec.php?exec_module=extension-with-delegated-authentication-endpoints-list&exec_page=src/Controller/FileInDelegatedAuthenticationEndpointsListAndAdminRequired.php",
[
'auth_user' => $sUserLogin,
'auth_pwd' => self::PASSWORD,
],
[],
true
);
$bShouldSeeForbiddenAdminPage ?
$this->assertStringContainsString('Access restricted to people having administrator privileges', $sPageContent, 'Should prevent non admin user to access this page') : // in delegated authentication endpoints list (in the module), login should not be required
$this->assertStringContainsString('Yo !', $sPageContent, 'Should execute the file and see its content since user has admin profile');
}
public function InDelegatedAuthenticationEndpointsWithAdminRequiredProvider()
{
return [
'Administrator profile' => [
self::$aURP_Profiles['Administrator'],
'Should see forbidden admin page' => false,
],
'ReadOnly profile' => [
self::$aURP_Profiles['Service Desk Agent'],
'Should see forbidden admin page' => true,
],
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'extension-with-delegated-authentication-endpoints-list/0.0.1',
[
// Identification
//
'label' => 'Templates foundation',
'category' => 'business',
// Setup
//
'dependencies' => [],
'mandatory' => true,
'visible' => false,
'installer' => 'TemplatesBaseInstaller',
// Security
'delegated_authentication_endpoints' => [
'src/Controller/FileInDelegatedAuthenticationEndpointsList.php',
'src/Controller/FileInDelegatedAuthenticationEndpointsListAndAdminRequired.php',
],
// Components
//
'datamodel' => [
'model.templates-base.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Select where, in the main UI, the extra data should be displayed:
// tab (dedicated tab)
// properties (right after the properties, but before the log if any)
// none (extra data accessed only by programs)
'view_extra_data' => 'relations',
],
]
);

View File

@@ -0,0 +1,10 @@
<?php
if (UserRights::IsLoggedIn()) {
throw new Exception("User should not be authenticated at this point");
}
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLogin(true);
echo 'Yo !';

View File

@@ -0,0 +1,45 @@
<?php
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'extension-without-delegated-authentication-endpoints-list/0.0.1',
[
// Identification
//
'label' => 'Templates foundation',
'category' => 'business',
// Setup
//
'dependencies' => [],
'mandatory' => true,
'visible' => false,
'installer' => 'TemplatesBaseInstaller',
// Components
//
'datamodel' => [
'model.templates-base.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [
// Select where, in the main UI, the extra data should be displayed:
// tab (dedicated tab)
// properties (right after the properties, but before the log if any)
// none (extra data accessed only by programs)
'view_extra_data' => 'relations',
],
]
);

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

@@ -834,6 +834,11 @@ HTML,
'good element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb', 'AD05nb'],
'bad element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb+', 'AD05nb'],
'array' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, ['AD05nb+','apply_modify'], ['AD05nb','apply_modify']],
'good module code' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module-code', 'some-module-code'],
'good module code with capitalized letters' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'SOME-module-code', 'SOME-module-code'],
'good module code with dot' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module-code-for-3.2-version', 'some-module-code-for-3.2-version'],
'bad module code with underscores' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some_module_code', null],
'bad module code with slashes' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module/code', null],
'good url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schools.com', 'https://www.w3schools.com'],
'bad url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https//www.w3schools.com', null],
'url with injection' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<img zzz src=x onerror=alert(1) //>', 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<imgzzzsrc=xonerror=alert(1)//>'],

View File

@@ -0,0 +1,44 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use AttributeDateTime;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DateTime;
use MetaModel;
class AttributeSubItemTest extends ItopDataTestCase
{
public const CREATE_TEST_ORG = true;
/**
* @param string $sAttCode
* @param string $sVerb
* @param string $sExpectedValue
*
* @return void
*/
public function testGetForTemplate()
{
$aUserRequestCustomParams = [
'title' => "Test DisplayStopwatch",
];
$oUserRequest = $this->CreateUserRequest(456, $aUserRequestCustomParams);
$iStartDate = time() - 200;
$oStopwatch = $oUserRequest->Get('ttr');
$oStopwatch->DefineThreshold(100, $iStartDate);
$oUserRequest->Set('ttr', $oStopwatch);
$sValue = $oUserRequest->Get('ttr_escalation_deadline');
$oAttDef = MetaModel::GetAttributeDef(get_class($oUserRequest), 'ttr_escalation_deadline');
self::assertEquals('Missed by 3 min', $oAttDef->GetForTemplate($sValue, 'html', $oUserRequest));
$oDateTime = new DateTime();
$oDateTime->setTimestamp($iStartDate);
$sDate = $oDateTime->format(AttributeDateTime::GetFormat());
self::assertEquals($sDate, $oAttDef->GetForTemplate($sValue, 'label', $oUserRequest), 'label() should render the date in the format specified in the configuration file, in parameter "date_and_time_format"');
self::assertEquals('Missed by 3 min', $oAttDef->GetForTemplate($sValue, 'text', $oUserRequest), 'text() should render the deadline as specified in the configuration file, in parameter "deadline_format", and depending on the user language');
self::assertEquals($iStartDate, $oAttDef->GetForTemplate($sValue, '', $oUserRequest));
}
}

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

@@ -9,6 +9,7 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use InlineImage;
use ormDocument;
class InlineImageTest extends ItopDataTestCase
{
@@ -98,4 +99,36 @@ HTML;
$this->assertStringContainsString(\utils::EscapeHtml(\utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.'123&s=abc'), $sResult);
$this->assertStringContainsString(\utils::EscapeHtml(\utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL.'456&s=def'), $sResult);
}
/**
* @covers InlineImage::ReplaceInlineImagesWithBase64Representation
*/
public function testReplaceInlineImagesWithBase64Representation()
{
// create an inline image in the database
$oInlineImage = $this->createObject(InlineImage::class, [
'expire' => (new \DateTime('+1 day'))->format('Y-m-d H:i:s'),
'item_class' => 'UserRequest',
'item_id' => 999,
'item_org_id' => 1,
'contents' => new ormDocument('0x89504E470D0A1A0A0000000D494844520000000E0000000E08060000001F482DD1000000017352474200AECE1CE90000000467414D410000B18F0BFC6105000000097048597300000EC300000EC301C76FA8640000001E49444154384F63782BA3F29F1CCC802E402C1ED588078F6AC483E9AF11008B8BA9C08A7A3F290000000049454E44AE426082', 'image/png', 'square_red.png'),
'secret' => 'a94bff3ea6a872bdbc359a1704cdddb3',
]);
$sInlineImageId = $oInlineImage->GetKey();
$sInlineImageSecret = $oInlineImage->Get('secret');
// HTML with inline image
$sHtml = <<<HTML
<img src="http://host/iTop/pages/ajax.document.php?operation=download_inlineimage&amp;id=$sInlineImageId&amp;s=$sInlineImageSecret" data-img-id="$sInlineImageId" data-img-secret="$sInlineImageSecret" />
HTML;
// expected HTML with base64 representation of the image
$sExpected = <<<HTML
<img src="data:image/png;base64,MHg4OTUwNEU0NzBEMEExQTBBMDAwMDAwMEQ0OTQ4NDQ1MjAwMDAwMDBFMDAwMDAwMEUwODA2MDAwMDAwMUY0ODJERDEwMDAwMDAwMTczNTI0NzQyMDBBRUNFMUNFOTAwMDAwMDA0Njc0MTRENDEwMDAwQjE4RjBCRkM2MTA1MDAwMDAwMDk3MDQ4NTk3MzAwMDAwRUMzMDAwMDBFQzMwMUM3NkZBODY0MDAwMDAwMUU0OTQ0NDE1NDM4NEY2Mzc4MkJBM0YyOUYxQ0NDODAyRTQwMkMxRUQ1ODgwNzhGNkFDNDgzRTlBRjExMDA4QjhCQTlDMDhBN0EzRjI5MDAwMDAwMDA0OTQ1NEU0NEFFNDI2MDgy" />
HTML;
// test the method
$sResult = InlineImage::ReplaceInlineImagesWithBase64Representation($sHtml);
$this->assertEquals($sExpected, $sResult);
}
}

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);
}
}