mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-09 19:58:44 +02:00
Compare commits
19 Commits
feature/91
...
feature/93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43121a5a4b | ||
|
|
ff2f10e5b6 | ||
|
|
ddaf014898 | ||
|
|
a71fefa328 | ||
|
|
00735f0c54 | ||
|
|
882390e8d6 | ||
|
|
5d0da47f21 | ||
|
|
4eadff7f3b | ||
|
|
f66ce1c956 | ||
|
|
802f9f3e08 | ||
|
|
5c5d98bb78 | ||
|
|
a08a9b43f3 | ||
|
|
abd85ff4db | ||
|
|
81f328b26e | ||
|
|
9a2c8f10bf | ||
|
|
3cdadf3c6e | ||
|
|
a6295f1b14 | ||
|
|
e467ca83cf | ||
|
|
7791585387 |
@@ -1546,7 +1546,16 @@ class ShortcutMenuNode extends MenuNode
|
||||
public function GetHyperlink($aExtraParams)
|
||||
{
|
||||
$sContext = $this->oShortcut->Get('context');
|
||||
$aContext = unserialize($sContext);
|
||||
try {
|
||||
$aContext = utils::Unserialize($sContext, ['allowed_classes' => false]);
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Warning("User shortcut corrupted, delete the shortcut", LogChannels::CONSOLE, [
|
||||
'shortcut_name' => $this->oShortcut->GetName(),
|
||||
'root_cause' => $e->getMessage(),
|
||||
]);
|
||||
// delete the shortcut
|
||||
$this->oShortcut->DBDelete();
|
||||
}
|
||||
if (isset($aContext['menu'])) {
|
||||
unset($aContext['menu']);
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ JS
|
||||
<<<HTML
|
||||
<form id="ObjectsAddForm_{$this->sInputid}">
|
||||
<div id="SearchResultsToAdd_{$this->sInputid}">
|
||||
<div style="background: #fff; border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
|
||||
<div style="border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
|
||||
</div>
|
||||
<input type="hidden" id="count_{$this->sInputid}" value="0"/>
|
||||
</form>
|
||||
|
||||
@@ -68,7 +68,7 @@ class UISearchFormForeignKeys
|
||||
<<<HTML
|
||||
<form id="ObjectsAddForm_{$this->m_iInputId}">
|
||||
<div id="SearchResultsToAdd_{$this->m_iInputId}" style="vertical-align:top;height:100%;overflow:auto;padding:0;border:0;">
|
||||
<div style="background: #fff; border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
|
||||
<div style="border:0; text-align:center; vertical-align:middle;"><p>{$sEmptyList}</p></div>
|
||||
</div>
|
||||
<input type="hidden" id="count_{$this->m_iInputId}" value="0"/>
|
||||
</form>
|
||||
|
||||
@@ -3252,4 +3252,50 @@ TXT
|
||||
|
||||
return $aTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP unserialize encapsulation, allow throwing exception when not allowed object class is detected (for security hardening)
|
||||
*
|
||||
* @param mixed $data data to unserialize
|
||||
* @param array $aOptions PHP @unserialise options
|
||||
* @param bool $bThrowNotAllowedObjectClassException flag to throw exception
|
||||
*
|
||||
* @return mixed PHP @unserialise return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function Unserialize(mixed $data, array $aOptions, bool $bThrowNotAllowedObjectClassException = true): mixed
|
||||
{
|
||||
$data = unserialize($data, $aOptions);
|
||||
|
||||
if ($bThrowNotAllowedObjectClassException) {
|
||||
try {
|
||||
self::AssertNoIncompleteClassDetected($data);
|
||||
} catch (Exception $e) {
|
||||
throw new CoreException('Unserialization failed because an incomplete class was detected.', [], '', $e);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that data provided doesn't contain any incomplete class.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function AssertNoIncompleteClassDetected(mixed $data): void
|
||||
{
|
||||
if (is_object($data)) {
|
||||
if ($data instanceof __PHP_Incomplete_Class) {
|
||||
throw new Exception('__PHP_Incomplete_Class_Name object detected');
|
||||
}
|
||||
foreach (get_object_vars($data) as $property) {
|
||||
self::AssertNoIncompleteClassDetected($property);
|
||||
}
|
||||
} elseif (is_array($data)) {
|
||||
foreach ($data as $value) {
|
||||
self::AssertNoIncompleteClassDetected($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4829,7 +4829,7 @@ class AttributeCaseLog extends AttributeLongText
|
||||
}
|
||||
|
||||
if (strlen($sIndex) > 0) {
|
||||
$aIndex = unserialize($sIndex);
|
||||
$aIndex = utils::Unserialize($sIndex, ['allowed_classes' => false], false);
|
||||
$value = new ormCaseLog($sLog, $aIndex);
|
||||
} else {
|
||||
$value = new ormCaseLog($sLog);
|
||||
|
||||
@@ -1730,6 +1730,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.disable_joined_classes_filter' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true, scope filters aren\'t applied to joined classes or union classes not directly listed in the SELECT clause.',
|
||||
'default' => true,
|
||||
'value' => true,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.hide_administrators' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true, non-administrator users will not be able to see the administrator accounts, the Administrator profile and the links between the administrator accounts and their profiles.',
|
||||
@@ -1738,11 +1746,11 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'security.force_login_when_no_delegated_authentication_endpoints_list' => [
|
||||
'security.disable_exec_forced_login_for_all_enpoints' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'If true, when no execution policy is defined, the user will be forced to log in (instead of being automatically logged in with the default profile)',
|
||||
'default' => false,
|
||||
'value' => false,
|
||||
'description' => 'If true, when no delegated authentication module is defined, no login will be forced on modules exec endpoints',
|
||||
'default' => true,
|
||||
'value' => true,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
|
||||
@@ -1925,4 +1925,37 @@ class DBObjectSearch extends DBSearch
|
||||
{
|
||||
return $this->GetCriteria()->ListParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DBObjectSearch
|
||||
*/
|
||||
protected function ApplyDataFilters(): DBObjectSearch
|
||||
{
|
||||
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$oSearch = $this;
|
||||
$aClassesToFilter = $this->GetSelectedClasses();
|
||||
|
||||
// Opt-in for joined classes filtering, otherwise only filter the selected class(es)
|
||||
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === false) {
|
||||
$aClassesToFilter = $this->GetJoinedClasses();
|
||||
}
|
||||
|
||||
// Apply filter (this is similar to the one in DBSearch but the factorization could make it less readable)
|
||||
foreach ($aClassesToFilter as $sClassAlias => $sClass) {
|
||||
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
|
||||
if ($oVisibleObjects === false) {
|
||||
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
|
||||
}
|
||||
if (is_object($oVisibleObjects)) {
|
||||
$oVisibleObjects->AllowAllData();
|
||||
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
|
||||
$oSearch->SetDataFiltered();
|
||||
}
|
||||
}
|
||||
return $oSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,21 +1122,7 @@ abstract class DBSearch
|
||||
*/
|
||||
protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null)
|
||||
{
|
||||
$oSearch = $this;
|
||||
if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered()) {
|
||||
foreach ($this->GetSelectedClasses() as $sClassAlias => $sClass) {
|
||||
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
|
||||
if ($oVisibleObjects === false) {
|
||||
// Make sure this is a valid search object, saying NO for all
|
||||
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
|
||||
}
|
||||
if (is_object($oVisibleObjects)) {
|
||||
$oVisibleObjects->AllowAllData();
|
||||
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
|
||||
$oSearch->SetDataFiltered();
|
||||
}
|
||||
}
|
||||
}
|
||||
$oSearch = $this->ApplyDataFilters();
|
||||
|
||||
if (is_array($aGroupByExpr)) {
|
||||
foreach ($aGroupByExpr as $sAlias => $oGroupByExp) {
|
||||
@@ -1608,4 +1594,33 @@ abstract class DBSearch
|
||||
* @return array{\VariableExpression}
|
||||
*/
|
||||
abstract public function GetExpectedArguments(): array;
|
||||
|
||||
/**
|
||||
* Apply data filters to the search, if needed
|
||||
*
|
||||
* @return DBSearch
|
||||
* @throws CoreException
|
||||
*/
|
||||
protected function ApplyDataFilters(): DBSearch
|
||||
{
|
||||
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$oSearch = $this;
|
||||
$aClassesToFilter = $this->GetSelectedClasses();
|
||||
|
||||
foreach ($aClassesToFilter as $sClassAlias => $sClass) {
|
||||
$oVisibleObjects = UserRights::GetSelectFilter($sClass, $this->GetModifierProperties('UserRightsGetSelectFilter'));
|
||||
if ($oVisibleObjects === false) {
|
||||
$oVisibleObjects = DBObjectSearch::FromEmptySet($sClass);
|
||||
}
|
||||
if (is_object($oVisibleObjects)) {
|
||||
$oVisibleObjects->AllowAllData();
|
||||
$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
|
||||
$oSearch->SetDataFiltered();
|
||||
}
|
||||
}
|
||||
return $oSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,4 +673,30 @@ class DBUnionSearch extends DBSearch
|
||||
|
||||
return $aVariableCriteria;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DBUnionSearch
|
||||
*/
|
||||
protected function ApplyDataFilters(): DBUnionSearch
|
||||
{
|
||||
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Opt-in for joined classes filtering, otherwise fallback on DBSearch filtering
|
||||
if (MetaModel::GetConfig()->Get('security.disable_joined_classes_filter') === true) {
|
||||
return parent::ApplyDataFilters();
|
||||
}
|
||||
|
||||
// Apply filters per sub-search
|
||||
$aFilteredSearches = [];
|
||||
foreach ($this->GetSearches() as $oSubSearch) {
|
||||
// Recursively call ApplyDataFilters on sub-searches
|
||||
$aFilteredSearches[] = $oSubSearch->ApplyDataFilters();
|
||||
}
|
||||
|
||||
$oSearch = new DBUnionSearch($aFilteredSearches);
|
||||
return $oSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ $ibo-search-form-panel--more-criteria--color: $ibo-color-blue-grey-800 !default;
|
||||
$ibo-search-form-panel--more-criteria--background-color: $ibo-color-white-100 !default;
|
||||
$ibo-search-form-panel--more-criteria--icon--color: $ibo-color-primary-600 !default;
|
||||
$ibo-search-form-panel--more-criteria--border-color: $ibo-search-form-panel--criteria--border-color !default;
|
||||
// calc is redundant but avoid SCSS min() from being used instead of CSS min()
|
||||
$ibo-search-form-panel--criteria--max-height: calc(min(#{$ibo-size-750}, 50vh)) !default;
|
||||
|
||||
$ibo-search-form-panel--items--hover--color: $ibo-color-grey-200 !default;
|
||||
|
||||
@@ -278,9 +280,10 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
|
||||
}
|
||||
|
||||
.sfc_form_group {
|
||||
display: block;
|
||||
margin-top: -1px;
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: -1px;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,11 +349,15 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
|
||||
display: none;
|
||||
max-width: 450px;
|
||||
width: max-content;
|
||||
max-height: 520px;
|
||||
max-height: $ibo-search-form-panel--criteria--max-height;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
.sfc_fg_operators {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
font-size: 12px;
|
||||
|
||||
.sfc_fg_operator {
|
||||
@@ -387,6 +394,9 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
|
||||
}
|
||||
|
||||
.sfc_opc_multichoices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
label > input {
|
||||
vertical-align: text-top;
|
||||
margin-left: $ibo-spacing-0;
|
||||
@@ -398,7 +408,6 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
|
||||
}
|
||||
|
||||
.sfc_opc_mc_items_wrapper {
|
||||
max-height: 415px; /* Must be less than .sfc_form_group:max-height - .sfc_opc_mc_toggler:height - .sfc_opc_mc_filter:height */
|
||||
overflow-y: auto;
|
||||
margin: $ibo-spacing-0 -8px; /* Compensate .sfc_opc_multichoices side padding so the hover style can take the full with */
|
||||
|
||||
@@ -560,8 +569,14 @@ $ibo-search-results-area--datatable-scrollhead--border--is-sticking: $ibo-search
|
||||
&.search_form_criteria_enum {
|
||||
.sfc_form_group {
|
||||
.sfc_fg_operator_in {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
> label {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
line-height: initial;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -279,11 +279,24 @@ try {
|
||||
$oRuntimeEnv = new RunTimeEnvironment('production', true);
|
||||
|
||||
try {
|
||||
SetupLog::Info('Move to production starts...');
|
||||
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
|
||||
if (!file_exists(APPROOT.'data/hub/compile_authent') || $sAuthent !== file_get_contents(APPROOT.'data/hub/compile_authent')) {
|
||||
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if (file_exists(APPROOT.'data/hub/compile_authent')) {
|
||||
unlink(APPROOT.'data/hub/compile_authent');
|
||||
}
|
||||
// Note: at this point, the dictionnary is not necessarily loaded
|
||||
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
|
||||
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
|
||||
ReportError($e->getMessage(), $e->getCode());
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
SetupLog::Info('Move to production starts...');
|
||||
|
||||
unlink(APPROOT.'data/hub/compile_authent');
|
||||
// Load the "production" config file to clone & update it
|
||||
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
|
||||
|
||||
@@ -189,11 +189,14 @@ $(function()
|
||||
this.buildData.script_code = '';
|
||||
this.buildData.style_code = '';
|
||||
|
||||
for (var i in oData.updated_fields)
|
||||
for (let i in oData.updated_fields)
|
||||
{
|
||||
var oUpdatedField = oData.updated_fields[i];
|
||||
this.options.fields_list[oUpdatedField.id] = oUpdatedField;
|
||||
this._prepareField(oUpdatedField.id);
|
||||
const oUpdatedField = oData.updated_fields[i];
|
||||
const oPreviousField = this.options.fields_list[oUpdatedField.id];
|
||||
if (!oPreviousField || JSON.stringify(oPreviousField) !== JSON.stringify(oUpdatedField)) {
|
||||
this.options.fields_list[oUpdatedField.id] = oUpdatedField;
|
||||
this._prepareField(oUpdatedField.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Adding code to the dom
|
||||
|
||||
@@ -193,7 +193,7 @@ return array(
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => $baseDir . '/sources/Application/Helper/SearchHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/Application/Helper/Session.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/Application/Helper/WebResourcesHelper.php',
|
||||
'Combodo\\iTop\\Application\\Newsroom\\iTopNewsroomProvider' => $baseDir . '/sources/Application/Newsroom/iTopNewsroomProvider.php',
|
||||
|
||||
@@ -548,7 +548,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/SearchHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/Application/Helper/Session.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/WebResourcesHelper.php',
|
||||
'Combodo\\iTop\\Application\\Newsroom\\iTopNewsroomProvider' => __DIR__ . '/../..' . '/sources/Application/Newsroom/iTopNewsroomProvider.php',
|
||||
|
||||
73
pages/UI.php
73
pages/UI.php
@@ -5,6 +5,7 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Helper\SearchHelper;
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
@@ -126,72 +127,6 @@ function SetObjectBreadCrumbEntry(DBObject $oObj, WebPage $oPage)
|
||||
$oPage->SetBreadCrumbEntry("ui-details-$sClass-".$oObj->GetKey(), $oObj->Get('friendlyname'), MetaModel::GetName($sClass).': '.$oObj->Get('friendlyname'), '', $sIcon, $sIconType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the result of a search request
|
||||
* @param $oP WebPage Web page for the output
|
||||
* @param $oFilter DBSearch The search of objects to display
|
||||
* @param $bSearchForm boolean Whether or not to display the search form at the top the page
|
||||
* @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
|
||||
* @param $sFormat string The format to use for the output: csv or html
|
||||
* @param $bDoSearch bool True to display the search results below the search form
|
||||
* @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course)
|
||||
* @throws \CoreException
|
||||
* @throws \DictExceptionMissingString
|
||||
*/
|
||||
function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = true, $aParams = [])
|
||||
{
|
||||
//search block
|
||||
$oBlockForm = null;
|
||||
if ($bSearchForm) {
|
||||
$aParams['open'] = $bSearchFormOpen;
|
||||
if (false === isset($aParams['table_id'])) {
|
||||
$aParams['table_id'] = 'result_1';
|
||||
}
|
||||
if (!empty($sBaseClass)) {
|
||||
$aParams['baseClass'] = $sBaseClass;
|
||||
}
|
||||
$oBlockForm = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams);
|
||||
|
||||
if (!$bDoSearch) {
|
||||
$oBlockForm->Display($oP, 0);
|
||||
}
|
||||
}
|
||||
if ($bDoSearch) {
|
||||
if (strtolower($sFormat) == 'csv') {
|
||||
$oBlock = new DisplayBlock($oFilter, 'csv', false);
|
||||
// Adjust the size of the Textarea containing the CSV to fit almost all the remaining space
|
||||
$oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block
|
||||
} else {
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
|
||||
// Breadcrumb
|
||||
//$iCount = $oBlock->GetDisplayedCount();
|
||||
$sPageId = "ui-search-".$oFilter->GetClass();
|
||||
$sLabel = MetaModel::GetName($oFilter->GetClass());
|
||||
$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
|
||||
}
|
||||
if ($bSearchForm) {
|
||||
//add search block
|
||||
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
|
||||
if ($sTableId == '') {
|
||||
$sTableId = 'result_1';
|
||||
}
|
||||
$aExtraParams['table_id'] = $sTableId;
|
||||
$aExtraParams['submit_on_load'] = false;
|
||||
$oUIBlockForm = $oBlockForm->GetDisplay($oP, 'search_1', $aExtraParams);
|
||||
//add result block
|
||||
$oUIBlock = $oBlock->GetDisplay($oP, $sTableId);
|
||||
$oUIBlock->AddCSSClasses(['display_block', 'sf_results_area']);
|
||||
$oUIBlock->AddDataAttribute('target', 'search_results');
|
||||
//$oUIBlockForm->AddSubBlock($oUIBlock);
|
||||
$oP->AddUiBlock($oUIBlockForm);
|
||||
$oUIBlockForm->AddSubBlock($oUIBlock);
|
||||
} else {
|
||||
$oBlock->Display($oP, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a form (checkboxes) to select the objects for which to apply a given action
|
||||
* Only the objects for which the action is valid can be checked. By default all valid objects are checked
|
||||
@@ -460,7 +395,7 @@ try {
|
||||
$sOQL = "SELECT $sOQLClass $sOQLClause";
|
||||
try {
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat);
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat);
|
||||
} catch (CoreException $e) {
|
||||
$oFilter = new DBObjectSearch($sOQLClass);
|
||||
$oSet = new DBObjectSet($oFilter);
|
||||
@@ -487,7 +422,7 @@ try {
|
||||
}
|
||||
$oP->set_title(Dict::S('UI:SearchResultsPageTitle'));
|
||||
$oFilter = new DBObjectSearch($sClass);
|
||||
DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */);
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */);
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -509,7 +444,7 @@ try {
|
||||
// $sParams = utils::ReadParam('aParams', '{}', false, \utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
// $aParams = json_decode($sParams, true);
|
||||
|
||||
DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); //, true, true, $aParams
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); //, true, true, $aParams
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Helper\ImportHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\Select;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\TextArea;
|
||||
@@ -389,14 +387,6 @@ EOF
|
||||
}
|
||||
break;
|
||||
|
||||
case 'display_classes_select':
|
||||
$oPage = new AjaxPage("");
|
||||
$sClassName = utils::ReadPostedParam('class_name', '', utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
$bAdvanced = utils::ReadPostedParam('advanced', 'false');
|
||||
$oClassesSelect = ImportHelper::GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, $bAdvanced === 'true');
|
||||
$oPage->AddSubBlock($oClassesSelect);
|
||||
break;
|
||||
|
||||
case 'get_csv_template':
|
||||
$sClassName = utils::ReadParam('class_name');
|
||||
$sFormat = utils::ReadParam('format', 'csv');
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Helper\ImportHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSectionUIBlockFactory;
|
||||
@@ -15,6 +14,7 @@ use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\FileSelect\FileSelectUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\Select;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\TextArea;
|
||||
@@ -30,6 +30,7 @@ use Combodo\iTop\Application\WebPage\AjaxPage;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Renderer\BlockRenderer;
|
||||
use Combodo\iTop\Service\Import\CSVImportPageProcessor;
|
||||
|
||||
try {
|
||||
@@ -51,6 +52,67 @@ try {
|
||||
$oPage = new iTopWebPage(Dict::S('UI:Title:BulkImport'));
|
||||
$oPage->SetBreadCrumbEntry('ui-tool-bulkimport', Dict::S('Menu:CSVImportMenu'), Dict::S('UI:Title:BulkImport+'), '', 'fas fa-file-import', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
|
||||
|
||||
/**
|
||||
* Helper function to build a select from the list of valid classes for a given action
|
||||
*
|
||||
* @deprecated 3.0.0 use GetClassesSelectUIBlock
|
||||
*
|
||||
* @param $sDefaultValue
|
||||
* @param integer $iWidthPx The width (in pixels) of the drop-down list
|
||||
* @param integer $iActionCode The ActionCode (from UserRights) to check for authorization for the classes
|
||||
*
|
||||
* @param string $sName The name of the select in the HTML form
|
||||
*
|
||||
* @return string The HTML fragment corresponding to the select tag
|
||||
*/
|
||||
function GetClassesSelect($sName, $sDefaultValue, $iWidthPx, $iActionCode = null)
|
||||
{
|
||||
DeprecatedCallsLog::NotifyDeprecatedPhpMethod('use GetClassesSelectUIBlock');
|
||||
$oSelectBlock = GetClassesSelectUIBlock($sName, $sDefaultValue, $iActionCode);
|
||||
|
||||
return BlockRenderer::RenderBlockTemplates($oSelectBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to build a select from the list of valid classes for a given action
|
||||
*
|
||||
* @param string $sName The name of the select in the HTML form
|
||||
* @param $sDefaultValue
|
||||
* @param integer $iWidthPx The width (in pixels) of the drop-down list
|
||||
* @param integer $iActionCode The ActionCode (from UserRights) to check for authorization for the classes
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\Input\Select\
|
||||
*/
|
||||
function GetClassesSelectUIBlock(string $sName, $sDefaultValue, int $iActionCode, bool $bAdvanced = false): Select
|
||||
{
|
||||
$oSelectBlock = SelectUIBlockFactory::MakeForSelect($sName, 'select_'.$sName);
|
||||
$oOption = SelectOptionUIBlockFactory::MakeForSelectOption("", Dict::S('UI:CSVImport:ClassesSelectOne'), false);
|
||||
$oSelectBlock->AddSubBlock($oOption);
|
||||
$aValidClasses = [];
|
||||
$aClassCategories = ['bizmodel', 'addon/authentication'];
|
||||
if ($bAdvanced) {
|
||||
$aClassCategories[] = 'grant_by_profile';
|
||||
}
|
||||
if (UserRights::IsAdministrator()) {
|
||||
$aClassCategories[] = 'application';
|
||||
}
|
||||
foreach ($aClassCategories as $sClassCategory) {
|
||||
foreach (MetaModel::GetClasses($sClassCategory) as $sClassName) {
|
||||
if ((is_null($iActionCode) || UserRights::IsActionAllowed($sClassName, $iActionCode)) &&
|
||||
(!MetaModel::IsAbstract($sClassName))) {
|
||||
$sDisplayName = ($bAdvanced) ? MetaModel::GetName($sClassName)." ($sClassName)" : MetaModel::GetName($sClassName);
|
||||
$aValidClasses[$sDisplayName] = SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sDisplayName, ($sClassName == $sDefaultValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($aValidClasses);
|
||||
foreach ($aValidClasses as $sValue => $oBlock) {
|
||||
$oSelectBlock->AddSubBlock($oBlock);
|
||||
}
|
||||
|
||||
return $oSelectBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to 'check' an input in an HTML form if the current value equals the value given
|
||||
*
|
||||
@@ -289,7 +351,7 @@ try {
|
||||
$oClassesSelect->AddSubBlock($oDefaultSelect);
|
||||
$aSynchroUpdate = utils::ReadParam('synchro_update', []);
|
||||
} else {
|
||||
$oClassesSelect = ImportHelper::GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, (bool)$bAdvanced);
|
||||
$oClassesSelect = GetClassesSelectUIBlock('class_name', $sClassName, UR_ACTION_BULK_MODIFY, (bool)$bAdvanced);
|
||||
}
|
||||
$oPanel = TitleUIBlockFactory::MakeForPage(Dict::S('UI:Title:CSVImportStep3'));
|
||||
$oPage->AddSubBlock($oPanel);
|
||||
@@ -313,9 +375,11 @@ try {
|
||||
$oAdvancedMode->GetInput()->SetIsChecked(($bAdvanced == 1));
|
||||
$oAdvancedMode->SetBeforeInput(false);
|
||||
$oAdvancedMode->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oAdvancedMode->SetDescription(utils::EscapeHtml(Dict::S('UI:CSVImport:AdvancedMode+')));
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oAdvancedMode));
|
||||
|
||||
$oDivAdvancedHelp = UIContentBlockUIBlockFactory::MakeStandard("advanced_help")->AddCSSClass('ibo-is-hidden');
|
||||
$oForm->AddSubBlock($oDivAdvancedHelp);
|
||||
|
||||
$oDivMapping = UIContentBlockUIBlockFactory::MakeStandard("mapping")->AddCSSClass('mt-5');
|
||||
$oMessage = AlertUIBlockFactory::MakeForInformation(Dict::S('UI:CSVImport:SelectAClassFirst'))->SetIsClosable(false)->SetIsCollapsible(false);
|
||||
$oDivMapping->AddSubBlock($oMessage);
|
||||
@@ -352,7 +416,7 @@ try {
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('#select_class_name').on('change', function(ev) { DoMapping(); } );
|
||||
$('#advanced').on('click', function(ev) { DoAdvanced(); } );
|
||||
$('#advanced').on('click', function(ev) { DoReload(); } );
|
||||
EOF
|
||||
);
|
||||
if ($sClassName != '') {
|
||||
@@ -365,13 +429,15 @@ EOF
|
||||
}
|
||||
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
<<<EOF
|
||||
var aDefaultKeys = new Array();
|
||||
var aReadOnlyKeys = new Array();
|
||||
|
||||
function DoAdvanced()
|
||||
function DoReload()
|
||||
{
|
||||
UpdateClassesSelect();
|
||||
$('input[name=step]').val(3);
|
||||
$('#wizForm').removeAttr('onsubmit'); // No need to perform validation checks when going back
|
||||
$('#wizForm').submit();
|
||||
}
|
||||
|
||||
function CSVGoBack()
|
||||
@@ -396,7 +462,14 @@ EOF
|
||||
{
|
||||
var class_name = $('select[name=class_name]').val();
|
||||
var advanced = $('input[name=advanced]:checked').val();
|
||||
|
||||
if (advanced != 1)
|
||||
{
|
||||
$('#advanced_help').hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#advanced_help').show();
|
||||
}
|
||||
if (class_name != '')
|
||||
{
|
||||
var separator = $('input[name=separator]').val();
|
||||
@@ -444,26 +517,6 @@ EOF
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateClassesSelect()
|
||||
{
|
||||
const aParams = {
|
||||
operation: 'display_classes_select',
|
||||
class_name: $('#select_class_name').val(),
|
||||
advanced: $('#advanced').is(':checked'),
|
||||
};
|
||||
|
||||
$('#select_class_name').block();
|
||||
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.csvimport.php',
|
||||
aParams,
|
||||
function(data) {
|
||||
$('#select_class_name').replaceWith($(data));
|
||||
$('#select_class_name').on('change', function(ev) { DoMapping(); } );
|
||||
DoMapping();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function CheckValues()
|
||||
{
|
||||
// Reset the highlight in case the check has already been executed with failure
|
||||
@@ -594,7 +647,7 @@ EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
JS
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1012,7 +1065,7 @@ EOF
|
||||
}*/
|
||||
//Tab:Template
|
||||
$oTabTemplate = $oTabContainer->AddTab('tabsTemplate', Dict::S('UI:CSVImport:Tab:Templates'));
|
||||
$oFieldTemplate = FieldUIBlockFactory::MakeFromObject(Dict::S('UI:CSVImport:PickClassForTemplate'), ImportHelper::GetClassesSelectUIBlock('template_class', '', UR_ACTION_BULK_MODIFY));
|
||||
$oFieldTemplate = FieldUIBlockFactory::MakeFromObject(Dict::S('UI:CSVImport:PickClassForTemplate'), GetClassesSelectUIBlock('template_class', '', UR_ACTION_BULK_MODIFY));
|
||||
$oTabTemplate->AddSubBlock($oFieldTemplate);
|
||||
$oDivTemplate = UIContentBlockUIBlockFactory::MakeStandard("template")->AddCSSClass("ibo-is-visible");
|
||||
$oTabTemplate->AddSubBlock($oDivTemplate);
|
||||
|
||||
@@ -104,7 +104,7 @@ if ($sTargetPage === false || $sModule === 'core' || $sModule === 'dictionaries'
|
||||
$aModuleDelegatedAuthenticationEndpointsList = GetModuleDelegatedAuthenticationEndpoints($sModule);
|
||||
// If module doesn't have the delegated authentication endpoints list defined, we rely on the conf. param. to decide if we force login or not.
|
||||
if (is_null($aModuleDelegatedAuthenticationEndpointsList)) {
|
||||
$bForceLoginWhenNoDelegatedAuthenticationEndpoints = utils::GetConfig()->Get('security.force_login_when_no_delegated_authentication_endpoints_list');
|
||||
$bForceLoginWhenNoDelegatedAuthenticationEndpoints = !utils::GetConfig()->Get('security.disable_exec_forced_login_for_all_enpoints');
|
||||
if ($bForceLoginWhenNoDelegatedAuthenticationEndpoints) {
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
LoginWebPage::DoLoginEx();
|
||||
|
||||
@@ -36,7 +36,7 @@ class SetupPage extends NiceWebPage
|
||||
{
|
||||
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/setuppage/layout';
|
||||
|
||||
protected const BODY_DATA_GUI_TYPE = 'setup';
|
||||
public const BODY_DATA_GUI_TYPE = 'setup';
|
||||
|
||||
public function __construct($sTitle)
|
||||
{
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Helper;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\Select;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\SelectUIBlockFactory;
|
||||
use CoreException;
|
||||
use Dict;
|
||||
use DictExceptionMissingString;
|
||||
use MetaModel;
|
||||
use UserRights;
|
||||
|
||||
/**
|
||||
* Class
|
||||
* ImportHelper
|
||||
*
|
||||
* @internal
|
||||
* @since 3.2.3
|
||||
* @package Combodo\iTop\Application\Helper
|
||||
*/
|
||||
class ImportHelper
|
||||
{
|
||||
/**
|
||||
* Get classes select UI block.
|
||||
*
|
||||
* @param string $sName
|
||||
* @param $sDefaultValue
|
||||
* @param int $iActionCode
|
||||
* @param bool $bAdvanced
|
||||
*
|
||||
* @return Select
|
||||
* @throws CoreException
|
||||
* @throws DictExceptionMissingString
|
||||
*/
|
||||
public static function GetClassesSelectUIBlock(string $sName, $sDefaultValue, int $iActionCode, bool $bAdvanced = false): Select
|
||||
{
|
||||
$oSelectBlock = SelectUIBlockFactory::MakeForSelect($sName, 'select_'.$sName);
|
||||
$oOption = SelectOptionUIBlockFactory::MakeForSelectOption("", Dict::S('UI:CSVImport:ClassesSelectOne'), false);
|
||||
$oSelectBlock->AddSubBlock($oOption);
|
||||
$aValidClasses = [];
|
||||
$aClassCategories = ['bizmodel', 'addon/authentication'];
|
||||
if ($bAdvanced) {
|
||||
$aClassCategories[] = 'grant_by_profile';
|
||||
}
|
||||
if (UserRights::IsAdministrator()) {
|
||||
$aClassCategories[] = 'application';
|
||||
}
|
||||
foreach ($aClassCategories as $sClassCategory) {
|
||||
foreach (MetaModel::GetClasses($sClassCategory) as $sClassName) {
|
||||
if ((is_null($iActionCode) || UserRights::IsActionAllowed($sClassName, $iActionCode)) &&
|
||||
(!MetaModel::IsAbstract($sClassName))) {
|
||||
$sDisplayName = ($bAdvanced) ? MetaModel::GetName($sClassName)." ($sClassName)" : MetaModel::GetName($sClassName);
|
||||
$aValidClasses[$sDisplayName] = SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sDisplayName, ($sClassName == $sDefaultValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($aValidClasses);
|
||||
foreach ($aValidClasses as $sValue => $oBlock) {
|
||||
$oSelectBlock->AddSubBlock($oBlock);
|
||||
}
|
||||
|
||||
return $oSelectBlock;
|
||||
}
|
||||
}
|
||||
87
sources/Application/Helper/SearchHelper.php
Normal file
87
sources/Application/Helper/SearchHelper.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\Helper;
|
||||
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use DBSearch;
|
||||
use DisplayBlock;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
class SearchHelper
|
||||
{
|
||||
/**
|
||||
* Displays the result of a search request
|
||||
* @param $oP WebPage Web page for the output
|
||||
* @param $oFilter DBSearch The search of objects to display
|
||||
* @param $bSearchForm boolean Whether or not to display the search form at the top the page
|
||||
* @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
|
||||
* @param $sFormat string The format to use for the output: csv or html
|
||||
* @param $bDoSearch bool True to display the search results below the search form
|
||||
* @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course)
|
||||
* @throws \CoreException
|
||||
* @throws \DictExceptionMissingString
|
||||
*/
|
||||
public static function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = true, $aParams = []): void
|
||||
{
|
||||
//search block
|
||||
$oBlockForm = null;
|
||||
if ($bSearchForm) {
|
||||
$aParams['open'] = $bSearchFormOpen;
|
||||
if (false === isset($aParams['table_id'])) {
|
||||
$aParams['table_id'] = 'result_1';
|
||||
}
|
||||
if (!empty($sBaseClass)) {
|
||||
$aParams['baseClass'] = $sBaseClass;
|
||||
}
|
||||
$oBlockForm = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams);
|
||||
|
||||
if (!$bDoSearch) {
|
||||
$oBlockForm->Display($oP, 0);
|
||||
}
|
||||
}
|
||||
if ($bDoSearch) {
|
||||
if (strtolower($sFormat) == 'csv') {
|
||||
$oBlock = new DisplayBlock($oFilter, 'csv', false);
|
||||
// Adjust the size of the Textarea containing the CSV to fit almost all the remaining space
|
||||
$oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block
|
||||
} else {
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
|
||||
// Breadcrumb
|
||||
//$iCount = $oBlock->GetDisplayedCount();
|
||||
$sPageId = "ui-search-".$oFilter->GetClass();
|
||||
$sLabel = MetaModel::GetName($oFilter->GetClass());
|
||||
$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', 'fas fa-search', iTopWebPage::ENUM_BREADCRUMB_ENTRY_ICON_TYPE_CSS_CLASSES);
|
||||
}
|
||||
if ($bSearchForm) {
|
||||
//add search block
|
||||
$sTableId = utils::ReadParam('_table_id_', null, false, 'raw_data');
|
||||
if ($sTableId == '') {
|
||||
$sTableId = 'result_1';
|
||||
}
|
||||
$aExtraParams['table_id'] = $sTableId;
|
||||
$aExtraParams['submit_on_load'] = false;
|
||||
$oUIBlockForm = $oBlockForm->GetDisplay($oP, 'search_1', $aExtraParams);
|
||||
|
||||
// If the class is not high cardinality, we can display the results directly in the same page
|
||||
if (!utils::IsHighCardinality($oFilter->GetClass())) {
|
||||
//add result block
|
||||
$oUIBlock = $oBlock->GetDisplay($oP, $sTableId);
|
||||
$oUIBlock->AddCSSClasses(['display_block', 'sf_results_area']);
|
||||
$oUIBlock->AddDataAttribute('target', 'search_results');
|
||||
$oUIBlockForm->AddSubBlock($oUIBlock);
|
||||
}
|
||||
|
||||
$oP->AddUiBlock($oUIBlockForm);
|
||||
} else {
|
||||
$oBlock->Display($oP, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,13 @@ use AttributeFriendlyName;
|
||||
use AttributeLinkedSet;
|
||||
use cmdbAbstract;
|
||||
use cmdbAbstractObject;
|
||||
use CoreException;
|
||||
use Dict;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use LogChannels;
|
||||
use Metamodel;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class DataTableSettings
|
||||
@@ -130,7 +135,10 @@ class DataTableSettings
|
||||
*/
|
||||
public function unserialize($sData)
|
||||
{
|
||||
$aData = unserialize($sData);
|
||||
$aData = utils::Unserialize($sData, ['allowed_classes' => false]);
|
||||
if (!is_array($aData)) {
|
||||
throw new CoreException('Wrong data table settings format, expected an array', ['datatable_settings_data' => $aData]);
|
||||
}
|
||||
$this->iDefaultPageSize = $aData['iDefaultPageSize'];
|
||||
$this->aColumns = $aData['aColumns'];
|
||||
foreach ($this->aClassAliases as $sAlias => $sClass) {
|
||||
@@ -269,7 +277,19 @@ class DataTableSettings
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$oSettings->unserialize($pref);
|
||||
|
||||
try {
|
||||
$oSettings->unserialize($pref);
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Warning("User table settings corrupted, back to the default values provided by the data model", LogChannels::CONSOLE, [
|
||||
'table_id' => $sTableId,
|
||||
'root_cause' => $e->getMessage(),
|
||||
]);
|
||||
// unset the preference
|
||||
appUserPreferences::UnsetPref($oSettings->GetPrefsKey($sTableId));
|
||||
// use the default values provided by the data model
|
||||
return null;
|
||||
}
|
||||
|
||||
return $oSettings;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ class ActivityPanelFactory
|
||||
if (false === empty($aRelatedTriggersIDs)) {
|
||||
// - Prepare query to retrieve events
|
||||
$oNotifEventsSearch = DBObjectSearch::FromOQL('SELECT EN FROM EventNotification AS EN JOIN Action AS A ON EN.action_id = A.id WHERE EN.trigger_id IN (:triggers_ids) AND EN.object_id = :object_id');
|
||||
$oNotifEventsSearch->AllowAllData();
|
||||
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $sObjId]);
|
||||
$oNotifEventsSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class WebPage implements Page
|
||||
*/
|
||||
public const DEFAULT_PAGE_TEMPLATE_REL_PATH = 'pages/backoffice/webpage/layout';
|
||||
|
||||
protected const BODY_DATA_GUI_TYPE = 'backoffice';
|
||||
public const BODY_DATA_GUI_TYPE = 'backoffice';
|
||||
|
||||
protected $s_title;
|
||||
protected $s_content;
|
||||
|
||||
@@ -7,6 +7,6 @@ oWidget{{ oUIBlock.oUILinksDirectWidget.GetInputId() }} = $('#{{ oUIBlock.oUILin
|
||||
input_name: '{{ oUIBlock.sInputName }}',
|
||||
submit_to: '{{ oUIBlock.sSubmitUrl }}',
|
||||
oWizardHelper: {{ oUIBlock.sWizHelper }},
|
||||
do_search: '{{ oUIBlock.sJSDoSearch }}'
|
||||
do_search: {{ oUIBlock.sJSDoSearch }}
|
||||
});
|
||||
{% endapply %}
|
||||
@@ -49,7 +49,7 @@
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-gui-type="{{ aPage.sBodyDataGuiType|default('backoffice') }}">
|
||||
<body data-gui-type="{{ aPage.sBodyDataGuiType|default(constant('Combodo\\iTop\\Application\\WebPage\\WebPage::BODY_DATA_GUI_TYPE')) }}">
|
||||
{% if aPage.isPrintable %}<div class="printable-content" style="width: 27.7cm;">{% endif %}
|
||||
{% block iboPageBodyHtml %}
|
||||
<div id="ibo-page-container">
|
||||
|
||||
@@ -26,14 +26,14 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
$this->BackupConfiguration();
|
||||
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
|
||||
if (file_exists($sFolderPath)) {
|
||||
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
|
||||
$this->RecurseRmdir($sFolderPath);
|
||||
}
|
||||
mkdir($sFolderPath);
|
||||
$this->RecurseCopy(__DIR__.'/extension-with-delegated-authentication-endpoints-list', $sFolderPath);
|
||||
|
||||
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
|
||||
if (file_exists($sFolderPath)) {
|
||||
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
|
||||
$this->RecurseRmdir($sFolderPath);
|
||||
}
|
||||
mkdir($sFolderPath);
|
||||
$this->RecurseCopy(__DIR__.'/extension-without-delegated-authentication-endpoints-list', $sFolderPath);
|
||||
@@ -81,8 +81,7 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
|
||||
public function testUserCanAccessAnyFile()
|
||||
{
|
||||
// generate random login
|
||||
$sUserLogin = 'user-'.date('YmdHis');
|
||||
$sUserLogin = 'user-'.uniqid();
|
||||
$this->CreateUser($sUserLogin, self::$aURP_Profiles['Service Desk Agent'], self::PASSWORD);
|
||||
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
|
||||
|
||||
@@ -102,7 +101,7 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
public function testWithoutDelegatedAuthenticationEndpointsListWithForceLoginConf()
|
||||
{
|
||||
@chmod($this->oConfig->GetLoadedFile(), 0770);
|
||||
$this->oConfig->Set('security.force_login_when_no_delegated_authentication_endpoints_list', true, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
|
||||
$this->oConfig->Set('security.disable_exec_forced_login_for_all_enpoints', false, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
|
||||
$this->oConfig->WriteToFile();
|
||||
@chmod($this->oConfig->GetLoadedFile(), 0444);
|
||||
$sPageContent = $this->CallItopUri(
|
||||
|
||||
@@ -1470,4 +1470,19 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
@chmod($sConfigPath, 0440);
|
||||
@unlink($this->sConfigTmpBackupFile);
|
||||
}
|
||||
protected function AddLoginModeAndSaveConfiguration(string $sLoginMode): void
|
||||
{
|
||||
$aAllowedLoginTypes = $this->oiTopConfig->GetAllowedLoginTypes();
|
||||
if (!in_array($sLoginMode, $aAllowedLoginTypes)) {
|
||||
$aAllowedLoginTypes[] = $sLoginMode;
|
||||
$this->oiTopConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
|
||||
$this->SaveItopConfFile();
|
||||
}
|
||||
}
|
||||
protected function SaveItopConfFile(): void
|
||||
{
|
||||
@chmod($this->oiTopConfig->GetLoadedFile(), 0770);
|
||||
$this->oiTopConfig->WriteToFile();
|
||||
@chmod($this->oiTopConfig->GetLoadedFile(), 0440);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +668,7 @@ abstract class ItopTestCase extends KernelTestCase
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $sUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
|
||||
curl_setopt($ch, CURLOPT_POST, $aCurlOptions[CURLOPT_POST] ?? 1);// set post data to true
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// Force disable of certificate check as most of dev / test env have a self-signed certificate
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
@@ -708,6 +708,16 @@ abstract class ItopTestCase extends KernelTestCase
|
||||
{
|
||||
$sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
|
||||
|
||||
// Add PHP version in header to be able to handle Docker dev environments with automatic PHP version detection (instead of hardcoding the PHP version in the app_root_url)
|
||||
$sPhpVersion = PHP_VERSION;
|
||||
$aPhpVersionParts = explode('.', $sPhpVersion);
|
||||
$sPhpVersionHeaderValue = ($aPhpVersionParts[0] ?? '0').($aPhpVersionParts[1] ?? '0');
|
||||
$aCurlOptions = $aCurlOptions ?? [];
|
||||
$aCurlOptions[CURLOPT_HTTPHEADER] = array_merge(
|
||||
$aCurlOptions[CURLOPT_HTTPHEADER] ?? [],
|
||||
['X-PHP-Version: '.$sPhpVersionHeaderValue]
|
||||
);
|
||||
|
||||
return $this->CallUrl($sUrl, $aPostFields, $aCurlOptions, $bXDebugEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Application\Helper;
|
||||
|
||||
use Combodo\iTop\Application\Helper\SearchHelper;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBSearch;
|
||||
use MetaModel;
|
||||
|
||||
class SearchHelperTest extends ItopDataTestCase
|
||||
{
|
||||
protected static array $aHighCardinalityClasses = [];
|
||||
protected static bool $bSearchManualSubmit = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::$aHighCardinalityClasses = MetaModel::GetConfig()->Get('high_cardinality_classes');
|
||||
self::$bSearchManualSubmit = MetaModel::GetConfig()->Get('search_manual_submit');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', static::$aHighCardinalityClasses);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', static::$bSearchManualSubmit);
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithNoHighCardinalityClassesAddsResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', false);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertTrue($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithHighCardinalityClassesDoesNotAddResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', ['UserRequest']);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', false);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithSearchManualSubmitAndWithoutHighCardinalityClassesDoesNotAddResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', true);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use lnkFunctionalCIToTicket;
|
||||
use MetaModel;
|
||||
use ormLinkSet;
|
||||
use UserRequest;
|
||||
use UserRights;
|
||||
|
||||
class DBSearchFilterJoinTest extends ItopDataTestCase
|
||||
{
|
||||
private const RESTRICTED_PROFILE = 'Configuration Manager';
|
||||
private $aData = [];
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->RequireOnceItopFile('application/startup.inc.php');
|
||||
$this->aData = $this->CreateDBSearchFilterTestData();
|
||||
DBSearch::EnableQueryCache(false, false);
|
||||
$this->LoginRestrictedUser($this->aData['allowed_org_id'], self::RESTRICTED_PROFILE);
|
||||
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testDBSearchFilterAppliedToJoinsWhenEnabled(string $sOql, int $iExpectedCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(true);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testDBSearchFilterAppliedToJoinsWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(false);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testAllowAllDataBypassesDBSearchFilterWhenEnabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(true);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testAllowAllDataBypassesDBSearchFilterWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(false);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
public function JoinedAndNestedOqlProvider(): array
|
||||
{
|
||||
return [
|
||||
'join-filter-on-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
],
|
||||
'nested-in-select' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF WHERE OSF.id IN (SELECT OSF1 FROM OSFamily AS OSF1 JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF1.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org)",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
|
||||
],
|
||||
'userrequest-join-person-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = VM.id JOIN UserRequest AS UR ON L.ticket_id = UR.id JOIN Person AS P ON UR.caller_id = P.id JOIN Organization AS O ON P.org_id = O.id WHERE O.name = :denied_org",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
],
|
||||
'union-join-filter-on-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org UNION SELECT OSF2 FROM OSFamily AS OSF2 JOIN VirtualMachine AS VM2 ON VM2.osfamily_id = OSF2.id JOIN Organization AS O2 ON VM2.org_id = O2.id WHERE O2.name = :allowed_org",
|
||||
'expected_filtered_count' => 1,
|
||||
'expected_unfiltered_count' => 2,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function EnableJoinFilterConfig(bool $bEnabled): void
|
||||
{
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$oConfig->Set('security.disable_joined_classes_filter', !$bEnabled);
|
||||
}
|
||||
|
||||
private function CreateDBSearchFilterTestData(): array
|
||||
{
|
||||
$sSuffix = 'DBSearchFilterJoinTest';
|
||||
|
||||
$sAllowedOrgName = 'DBSearchFilterAllowedOrg-'.$sSuffix;
|
||||
$iAllowedOrgId = $this->GivenObjectInDB('Organization', [
|
||||
'name' => $sAllowedOrgName,
|
||||
]);
|
||||
|
||||
$this->debug("Org allowed id: $iAllowedOrgId");
|
||||
$sDeniedOrgName = 'DBSearchFilterDeniedOrg-'.$sSuffix;
|
||||
$iDeniedOrgId = $this->GivenObjectInDB('Organization', [
|
||||
'name' => $sDeniedOrgName,
|
||||
]);
|
||||
$this->debug("Org denied id: $iDeniedOrgId");
|
||||
|
||||
$iDeniedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
|
||||
'name' => 'DBSearchFilterOsFamilyDenied-'.$sSuffix,
|
||||
]);
|
||||
|
||||
$iAllowedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
|
||||
'name' => 'DBSearchFilterOsFamilyAllowed-'.$sSuffix,
|
||||
]);
|
||||
|
||||
$iDeniedVMId = $this->GivenObjectInDB('VirtualMachine', [
|
||||
'name' => 'DBSearchFilterVmDenied-'.$sSuffix,
|
||||
'org_id' => $iDeniedOrgId,
|
||||
'osfamily_id' => $iDeniedOsFamilyId,
|
||||
'virtualhost_id' => 1,
|
||||
]);
|
||||
|
||||
$iVirtualHostId = $this->GivenObjectInDB('Hypervisor', [
|
||||
'name' => 'DBSearchFilterVHost-'.$sSuffix,
|
||||
'org_id' => $iAllowedOrgId,
|
||||
]);
|
||||
|
||||
$this->GivenObjectInDB('VirtualMachine', [
|
||||
'name' => 'DBSearchFilterVmAllowed-'.$sSuffix,
|
||||
'org_id' => $iAllowedOrgId,
|
||||
'osfamily_id' => $iAllowedOsFamilyId,
|
||||
'virtualhost_id' => $iVirtualHostId,
|
||||
]);
|
||||
|
||||
$oDeniedPerson = $this->CreatePerson('Denied-'.$sSuffix, $iDeniedOrgId);
|
||||
|
||||
$oUserRequest = $this->CreateUserRequest('Denied'.$sSuffix, [
|
||||
'caller_id' => $oDeniedPerson->GetKey(),
|
||||
'org_id' => $iDeniedOrgId,
|
||||
]);
|
||||
|
||||
// Add Virtual Machine to UserRequest lnk
|
||||
$oLinkSet = new ormLinkSet(UserRequest::class, 'functionalcis_list', DBObjectSet::FromScratch(lnkFunctionalCIToTicket::class));
|
||||
|
||||
$oLink = MetaModel::NewObject(lnkFunctionalCIToTicket::class, ['functionalci_id' => $iDeniedVMId]);
|
||||
$oLinkSet->AddItem($oLink);
|
||||
|
||||
$oUserRequest->Set('functionalcis_list', $oLinkSet);
|
||||
$oUserRequest->DBUpdate();
|
||||
|
||||
return [
|
||||
'allowed_org_id' => $iAllowedOrgId,
|
||||
'allowed_org_name' => $sAllowedOrgName,
|
||||
'denied_org_name' => $sDeniedOrgName,
|
||||
];
|
||||
}
|
||||
|
||||
private function LoginRestrictedUser(int $iAllowedOrgId, string $sProfileName): void
|
||||
{
|
||||
$sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($iAllowedOrgId, self::$aURP_Profiles[$sProfileName]);
|
||||
UserRights::Login($sLogin);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
180
tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php
Normal file
180
tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Pages;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use Dict;
|
||||
use UserLocal;
|
||||
use UserRequest;
|
||||
|
||||
class AjaxRenderTest extends ItopDataTestCase
|
||||
{
|
||||
public const USE_TRANSACTION = false;
|
||||
public const AUTHENTICATION_PASSWORD = "tagada-Secret,007";
|
||||
|
||||
private static string $sLogin;
|
||||
private static int $iTicketId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->BackupConfiguration();
|
||||
$this->oiTopConfig->Set('log_level_min', 'Error');
|
||||
$this->oiTopConfig->Set('login_debug', true);
|
||||
|
||||
$this->CreateTestOrganization();
|
||||
|
||||
// Add URL authentication mode
|
||||
$this->AddLoginModeAndSaveConfiguration('url');
|
||||
|
||||
// Create ticket
|
||||
$description = date('dmY H:i:s');
|
||||
$oTicket = $this->createObject('UserRequest', [
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
"title" => "Houston, got a problem",
|
||||
"description" => $description,
|
||||
]);
|
||||
self::$iTicketId = $oTicket->GetKey();
|
||||
}
|
||||
|
||||
// Test that if a user with the right permissions tries to acquire the lock on a ticket, it succeeds and returns the correct success message
|
||||
public function testAcquireLockSuccess(): void
|
||||
{
|
||||
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
|
||||
$this->assertStringContainsString('"success":true', $sOutput);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object that does not exist, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfObjectDoesNotExist(): void
|
||||
{
|
||||
// Create a user with Support Agent Profile
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
|
||||
// Try to acquire the lock on a non-existent object
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, 99999999);
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// Check that the error log contains the expected error message about the object not existing
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object for which they don't have modification rights, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfUserHasNoModifyRights(): void
|
||||
{
|
||||
// Create a user with a profile without modification rights on UserRequest
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Configuration Manager']);
|
||||
|
||||
// Try to acquire the lock on the ticket
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// The user should not have the rights to acquire the lock, and an error should be logged
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object that belongs to another organization, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfObjectInOtherOrg(): void
|
||||
{
|
||||
// Create an organization and a ticket in this organization
|
||||
$iOtherOrgId = $this->createObject('Organization', ['name' => 'OtherOrg'])->GetKey();
|
||||
$oTicket = $this->createObject('UserRequest', [
|
||||
'org_id' => $iOtherOrgId,
|
||||
'title' => 'Ticket autre org',
|
||||
'description' => 'Test',
|
||||
]);
|
||||
|
||||
// Create a user who only has access to the main test organization
|
||||
$oUser = $this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
$oAllowedOrgList = $oUser->Get('allowed_org_list');
|
||||
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $this->getTestOrgId()]);
|
||||
$oAllowedOrgList->AddItem($oUserOrg);
|
||||
$oUser->Set('allowed_org_list', $oAllowedOrgList);
|
||||
$oUser->DBWrite();
|
||||
|
||||
// Try to acquire the lock on the ticket of the other organization
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, $oTicket->GetKey());
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// The user should not have access to the ticket of the other organization, so an error should be logged
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user has already acquired the lock on an object, another user cannot acquire it and gets the correct error message
|
||||
public function testAcquireLockFailsIfAlreadyLockedByAnotherUser(): void
|
||||
{
|
||||
// First, acquire the lock with a user (User A)
|
||||
$this->CreateSupportAgentUserAndAcquireLock();
|
||||
$sUserALogin = self::$sLogin;
|
||||
|
||||
// Create a second user (User B) who tries to acquire the lock
|
||||
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
|
||||
|
||||
// The second user should not be able to acquire the lock, and the output should contain the correct error message indicating that the object is already locked by User A
|
||||
$this->assertStringContainsString('"success":false', $sOutput);
|
||||
$this->assertStringContainsString('"message":"'.Dict::Format('UI:CurrentObjectIsSoftLockedBy_User', $sUserALogin).'"', $sOutput);
|
||||
}
|
||||
|
||||
// Helper method to create a user with Support Agent profile and acquire the lock on the ticket
|
||||
private function CreateSupportAgentUserAndAcquireLock(): string
|
||||
{
|
||||
// Create a user with Support Agent Profile
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
|
||||
return $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
|
||||
}
|
||||
|
||||
// Helper method to create a user with a specific profile
|
||||
private function CreateUserWithProfile(int $iProfileId): UserLocal
|
||||
{
|
||||
self::$sLogin = uniqid('AjaxRenderTest');
|
||||
return $this->CreateContactlessUser(self::$sLogin, $iProfileId, self::AUTHENTICATION_PASSWORD);
|
||||
}
|
||||
|
||||
// Helper method to acquire the lock on a ticket as a specific user
|
||||
private function AcquireLockAsUser(string $sLogin, int $iTicketId): string
|
||||
{
|
||||
$aGetFields = [
|
||||
'operation' => 'acquire_lock',
|
||||
'auth_user' => $sLogin,
|
||||
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
|
||||
'obj_class' => UserRequest::class,
|
||||
'obj_key' => $iTicketId,
|
||||
];
|
||||
|
||||
return $this->CallItopUri(
|
||||
"pages/ajax.render.php?".http_build_query($aGetFields),
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
|
||||
CURLOPT_POST => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the last lines of the error log containing only errors (Error level)
|
||||
private function GetErrorLogLastLines(string $sErrorLogPath, int $iLineNumbers = 1): string
|
||||
{
|
||||
if (!file_exists($sErrorLogPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$aLines = file($sErrorLogPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
// Keep only lines containing '| Error |'
|
||||
$aErrorLines = array_filter($aLines, function ($line) {
|
||||
return preg_match('/\|\s*Error\s*\|/', $line);
|
||||
});
|
||||
|
||||
// Return the last requested lines
|
||||
return implode("\n", array_slice($aErrorLines, -$iLineNumbers));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user