mirror of
https://github.com/Combodo/iTop.git
synced 2026-03-28 04:14:13 +01:00
Compare commits
21 Commits
feature/93
...
feature/92
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55faa8d504 | ||
|
|
7f8ec25977 | ||
|
|
41f8437c23 | ||
|
|
df8b25d4b4 | ||
|
|
511dabe2b0 | ||
|
|
0c517f254c | ||
|
|
c56c7a1f9d | ||
|
|
fb2f0f1447 | ||
|
|
b3223eb9b6 | ||
|
|
458a996c29 | ||
|
|
c61b21559c | ||
|
|
ed33238750 | ||
|
|
272678b8cd | ||
|
|
170014e8f0 | ||
|
|
006f666089 | ||
|
|
2a16143e53 | ||
|
|
eabbe2f00b | ||
|
|
58790bc352 | ||
|
|
28db230697 | ||
|
|
4fe61cbdc7 | ||
|
|
e2994b645b |
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
30
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1738,6 +1738,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)',
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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" => "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ SetupWebPage::AddModule(
|
||||
'doc.manual_setup' => '',
|
||||
'doc.more_information' => '',
|
||||
|
||||
// Security
|
||||
'delegated_authentication_endpoints' => [
|
||||
'ajax.backup.php',
|
||||
],
|
||||
|
||||
// Default settings
|
||||
//
|
||||
'settings' => [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 !== '') )
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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~~',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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~~',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => 'Удалить изображение',
|
||||
|
||||
@@ -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~~',
|
||||
|
||||
@@ -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~~',
|
||||
|
||||
@@ -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' => '移除图片',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,40 @@ if ($sTargetPage === false) {
|
||||
//
|
||||
// GO!
|
||||
//
|
||||
// check module white list
|
||||
// check conf param
|
||||
// force login if needed
|
||||
|
||||
$aModuleDelegatedAuthenticationEndpointsList = GetModuleDelegatedAuthenticationEndpoints($sModule);
|
||||
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 (is_array($aModuleDelegatedAuthenticationEndpointsList) && !in_array($sPage, $aModuleDelegatedAuthenticationEndpointsList)) {
|
||||
// if module defined a delegated authentication endpoints but not for the current page, we consider that the page is not allowed to be executed without login
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
LoginWebPage::DoLoginEx();
|
||||
}
|
||||
if (is_null($aModuleDelegatedAuthenticationEndpointsList) && !UserRights::IsLoggedIn()) {
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
// check if user is not logged in, if not log a warning in the log file as the page is executed without login, which is not recommended for security reason
|
||||
IssueLog::Debug("The '$sPage' page is executed without logging in. This call will be blocked in the future and will likely cause unwanted behaviour in the '$sModule' module. Please define a delegated authentication endpoint for the module, as described at https://www.itophub.io/wiki/page?id=latest:customization:new_extension#security.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ class iTopExtensionsMap
|
||||
* @param string $sModuleFile
|
||||
* @return array
|
||||
*/
|
||||
protected function GetModuleInfo($sModuleFile)
|
||||
public function GetModuleInfo($sModuleFile)
|
||||
{
|
||||
static $iDummyClassIndex = 0;
|
||||
|
||||
|
||||
@@ -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" => "",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -42,6 +42,7 @@ use RunTimeEnvironment;
|
||||
use ScalarExpression;
|
||||
use SetupUtils;
|
||||
use UILinksWidget;
|
||||
use UserRights;
|
||||
use utils;
|
||||
use WizardHelper;
|
||||
|
||||
@@ -71,6 +72,12 @@ 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) {
|
||||
throw new Exception(Dict::Format('UI:Error:ReadNotAllowedOn_Class', $oSet->GetClass()));
|
||||
}
|
||||
|
||||
$aResult["draw"] = $iDrawNumber;
|
||||
$aResult["recordsTotal"] = $oSet->Count();
|
||||
$aResult["recordsFiltered"] = $aResult["recordsTotal"] ;
|
||||
@@ -95,6 +102,11 @@ class AjaxRenderController
|
||||
continue;
|
||||
}
|
||||
|
||||
// N°8606 : Check user permissions on the current class
|
||||
if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) !== UR_ALLOWED_YES) {
|
||||
throw new Exception(Dict::Format('UI:Error:ReadNotAllowedOn_Class', $sClass));
|
||||
}
|
||||
|
||||
foreach ($aColumnsLoad[$sAlias] as $sAttCode) {
|
||||
$aObj[$sAlias."/".$sAttCode] = $aObject[$sAlias]->GetAsHTML($sAttCode);
|
||||
$bExcludeRawValue = false;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
]
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo 'Yo !';
|
||||
@@ -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 !';
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo 'Yo !';
|
||||
@@ -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',
|
||||
],
|
||||
]
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo 'Yo !';
|
||||
@@ -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)//>'],
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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&id=$sInlineImageId&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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user