From 87977863c9ec1e22938174f44c6ffef556142e4b Mon Sep 17 00:00:00 2001 From: Anne-Cath Date: Fri, 24 Apr 2026 16:22:41 +0200 Subject: [PATCH 1/3] =?UTF-8?q?N=C2=B09542=20-=20Portal=20:=20add=20the=20?= =?UTF-8?q?possibility=20to=20choose=20subclass=20in=20search=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/config/routes/object_brick.yaml | 7 + .../src/Controller/ObjectController.php | 181 +++++++++++++++--- .../object/mode_search_regular.html.twig | 46 ++++- 3 files changed, 205 insertions(+), 29 deletions(-) diff --git a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml index a86d8e1257..0597ab0dba 100644 --- a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml +++ b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml @@ -52,6 +52,13 @@ p_object_search_from_attribute: sHostObjectClass: ~ sHostObjectId: ~ +p_columns_from_attribute_with_class: + path: '/object/search/columns-from-attribute-with-class/{sTargetAttCode}/{sHostObjectClass}/{sHostObjectId}' + defaults: + _controller: 'Combodo\iTop\Portal\Controller\ObjectController::GetColumnsFromAttributeAction' + sHostObjectClass: ~ + sHostObjectId: ~ + p_object_search_autocomplete: path: '/object/search/autocomplete/{sTargetAttCode}/{sHostObjectClass}/{sHostObjectId}' defaults: diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index 5ccf29f7a2..667b9100f4 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -714,6 +714,125 @@ public function SearchAutocompleteAction(Request $oRequest, $sTargetAttCode, $sH return $oResponse; } + public function GetColumnsFromAttributeAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) + { + $sFinalClass = $this->oRequestManipulatorHelper->ReadParam('finalclass', null, FILTER_UNSAFE_RAW); + /** @var array $aCombodoPortalInstanceConf */ + $aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf'); + + $aData = [ + 'sMode' => 'search_regular', + 'sTargetAttCode' => $sTargetAttCode, + 'sHostObjectClass' => $sHostObjectClass, + 'sHostObjectId' => $sHostObjectId, + 'sActionRulesToken' => $this->oRequestManipulatorHelper->ReadParam('ar_token', ''), + ]; + + // Checking security layers + if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) { + IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sHostObjectClass.'::'.$sHostObjectId.' object.'); + throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving host object for future DBSearch parameters + if ($sHostObjectId !== null) { + // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated + $oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true); + } else { + $oHostObject = MetaModel::NewObject($sHostObjectClass); + // Retrieving action rules + // + // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. + // But it would not be a security issue as it only presets values in the form. + $aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : []; + // Preparing object + $this->oContextManipulatorHelper->PrepareObject($aActionRules, $oHostObject); + } + + // Updating host object with form data / values + $sFormManagerClass = $this->oRequestManipulatorHelper->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); + $sFormManagerData = $this->oRequestManipulatorHelper->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); + if (!empty($sFormManagerClass) && !empty($sFormManagerData)) { + /** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */ + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + $oFormManager->SetObjectFormHandlerHelper($this->oObjectFormHandlerHelper); + $oFormManager->SetObject($oHostObject); + + // Applying action rules if present + if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) { + $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); + $oObj = $oFormManager->GetObject(); + $this->oContextManipulatorHelper->PrepareObject($aActionRules, $oObj); + $oFormManager->SetObject($oObj); + } + + // Updating host object + $oFormManager->OnUpdate([ + 'currentValues' => $this->oRequestManipulatorHelper->ReadParam('current_values', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY), + ]); + $oHostObject = $oFormManager->GetObject(); + } + + // Retrieving request parameters + $sFieldId = $this->oRequestManipulatorHelper->ReadParam('sFieldId', ''); + + // Building search query + // - Retrieving target object class from attcode + $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode); + if ($oTargetAttDef->IsExternalKey()) { + /** @var \AttributeExternalKey $oTargetAttDef */ + $sTargetObjectClass = $oTargetAttDef->GetTargetClass(); + } elseif ($oTargetAttDef->IsLinkSet()) { + /** @var \AttributeLinkedSet $oTargetAttDef */ + if (!$oTargetAttDef->IsIndirect()) { + $sTargetObjectClass = $oTargetAttDef->GetLinkedClass(); + } else { + /** @var \AttributeLinkedSetIndirect $oTargetAttDef */ + /** @var \AttributeExternalKey $oRemoteAttDef */ + $oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote()); + $sTargetObjectClass = $oRemoteAttDef->GetTargetClass(); + } + } elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') { + $oRequestTemplate = $oHostObject->Get($sTargetAttCode); + /** @var \DBSearch $oTemplateFieldSearch */ + $oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch(); + $sTargetObjectClass = $oTemplateFieldSearch->GetClass(); + } else { + throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, '.get_class($oTargetAttDef).' given.'); + } + if (!empty($sFinalClass)) { + if (!MetaModel::IsParentClass($sTargetObjectClass, $sFinalClass)) { + throw new Exception('The finalclass parameter should be a child class of the target object class'); + } + } else { + $sFinalClass = $sTargetObjectClass; + } + + // - Retrieving class attribute list + $aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sFinalClass, 'list'); + // - Adding friendlyname attribute to the list is not already in it + $sTitleAttCode = 'friendlyname'; + if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) { + $aAttCodes = array_merge([$sTitleAttCode], $aAttCodes); + } + + // Retrieving results + // - Retrieving columns properties + $aColumnProperties = []; + foreach ($aAttCodes as $sAttCode) { + $oAttDef = MetaModel::GetAttributeDef($sFinalClass, $sAttCode); + $aColumnProperties[$sAttCode] = [ + 'title' => $oAttDef->GetLabel(), + ]; + } + + // Preparing response + $aData = $aData + [ + 'levelsProperties' => $aColumnProperties, + ]; + + return new JsonResponse($aData); + } /** * Handles the regular (table) search from an attribute * @@ -737,6 +856,7 @@ public function SearchAutocompleteAction(Request $oRequest, $sTargetAttCode, $sH */ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) { + $sFinalClass = $this->oRequestManipulatorHelper->ReadParam('finalclass', null, FILTER_UNSAFE_RAW); /** @var array $aCombodoPortalInstanceConf */ $aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf'); @@ -826,9 +946,16 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s } else { throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, '.get_class($oTargetAttDef).' given.'); } + if (utils::IsNotNullOrEmptyString($sFinalClass)) { + if (!MetaModel::IsParentClass($sTargetObjectClass, $sFinalClass)) { + throw new Exception('The finalclass parameter should be a child class of the target object class'); + } + } else { + $sFinalClass = $sTargetObjectClass; + } // - Retrieving class attribute list - $aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sTargetObjectClass, 'list'); + $aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sFinalClass, 'list'); // - Adding friendlyname attribute to the list is not already in it $sTitleAttCode = 'friendlyname'; if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) { @@ -838,10 +965,10 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s // - Retrieving scope search // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. // It is the responsibility of the template designer to write the right query so the user see only what he should. - $oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ); + $oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sFinalClass, UR_ACTION_READ); $aInternalParams = []; if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) { - IssueLog::Info(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' has no scope query for '.$sTargetObjectClass.' class.'); + IssueLog::Info(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' has no scope query for '.$sFinalClass.' class.'); throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); } @@ -855,7 +982,9 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s // Note : $oTemplateFieldSearch has been defined in the "Retrieving target object class from attcode" part, it is not available otherwise $oSearch = $oTemplateFieldSearch; } - + if ($sFinalClass != $sTargetObjectClass) { + $oSearch->AddCondition('finalclass', $sFinalClass, '='); + } // - Filtering objects to ignore if (($aObjectIdsToIgnore !== null) && (is_array($aObjectIdsToIgnore))) { //$oSearch->AddConditionExpression('id', $aObjectIdsToIgnore, 'NOT IN'); @@ -877,7 +1006,7 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s /** @noinspection SlowArrayOperationsInLoopInspection */ for ($i = 0; $i < count($aAttCodes); $i++) { // Checking if the current attcode is an external key in order to search on the friendlyname - $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $aAttCodes[$i]); + $oAttDef = MetaModel::GetAttributeDef($sFinalClass, $aAttCodes[$i]); $sAttCode = (!$oAttDef->IsExternalKey()) ? $aAttCodes[$i] : $aAttCodes[$i].'_friendlyname'; // Building expression for the current attcode // - For attributes that need conversion from their display value to storage value @@ -933,38 +1062,25 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s } } - // Retrieving results - // - Preparing object set - $oSet = new DBObjectSet($oSearch, [], $aInternalParams); - $oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aAttCodes]); - $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); // - Retrieving columns properties $aColumnProperties = []; foreach ($aAttCodes as $sAttCode) { - $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode); + $oAttDef = MetaModel::GetAttributeDef($sFinalClass, $sAttCode); $aColumnProperties[$sAttCode] = [ 'title' => $oAttDef->GetLabel(), ]; } - // - Retrieving objects - $aItems = []; - while ($oItem = $oSet->Fetch()) { - $aItems[] = $this->PrepareObjectInformation($oItem, $aAttCodes); - } // Preparing response if ($bInitialPass) { $aData = $aData + [ + 'sParentClass' => $sTargetObjectClass, 'form' => [ 'id' => 'object_search_form_'.time(), 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel()), 'title_complement' => MetaModel::GetName($sTargetObjectClass), ], 'aColumnProperties' => json_encode($aColumnProperties), - 'aResults' => [ - 'aItems' => json_encode($aItems), - 'iCount' => count($aItems), - ], 'bMultipleSelect' => $oTargetAttDef->IsLinkSet(), 'aSource' => [ 'sFormPath' => $sFormPath, @@ -974,7 +1090,19 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s 'sFormManagerData' => $sFormManagerData, ], ]; - + if (MetaModel::HasChildrenClasses($sTargetObjectClass)) { + $aEnumChildClasses = \MetaModel::EnumChildClasses('FunctionalCI'); + $aChildClasses = []; + foreach ($aEnumChildClasses as $sClassName) { + $aChildClasses[$sClassName] = MetaModel::GetName($sClassName); + } + $aData = $aData + [ + 'bHasSubClasses' => true, + 'aSubClasses' => $aChildClasses, + ]; + } else { + $aData = $aData + ['bHasSubClasses' => false]; + } if ($oRequest->isXmlHttpRequest()) { $oResponse = $this->render($this->GetTemplatePath('modal'), $aData); } else { @@ -982,13 +1110,22 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s $oResponse = $this->render($this->GetTemplatePath('page'), $aData); } } else { + // Retrieving results + // - Preparing object set + $oSet = new DBObjectSet($oSearch, [], $aInternalParams); + $oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aAttCodes]); + $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); + // - Retrieving objects + $aItems = []; + while ($oItem = $oSet->Fetch()) { + $aItems[] = $this->PrepareObjectInformation($oItem, $aAttCodes); + } $aData = $aData + [ 'levelsProperties' => $aColumnProperties, 'data' => $aItems, 'recordsTotal' => $oSet->Count(), 'recordsFiltered' => $oSet->Count(), ]; - $oResponse = new JsonResponse($aData); } diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig index 04a1449a82..d48de60e86 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig @@ -8,8 +8,19 @@
{#
#} + {% if bHasSubClasses %} +
+ + +
+ {% endif %}
- +
@@ -26,13 +37,13 @@ \ No newline at end of file From 46222d750ffad68f7d33dd0aadfe30fae04b6dbf Mon Sep 17 00:00:00 2001 From: Anne-Cath Date: Fri, 24 Apr 2026 17:07:17 +0200 Subject: [PATCH 2/3] =?UTF-8?q?N=C2=B09542=20-=20remove=20the=20hard=20val?= =?UTF-8?q?ues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/src/Controller/ObjectController.php | 4 ++-- .../templates/bricks/object/mode_search_regular.html.twig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index 667b9100f4..5a75fa5709 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -1074,7 +1074,7 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s // Preparing response if ($bInitialPass) { $aData = $aData + [ - 'sParentClass' => $sTargetObjectClass, + 'sParentClassName' => Dict::S('Class:'.$sTargetObjectClass), 'form' => [ 'id' => 'object_search_form_'.time(), 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel()), @@ -1091,7 +1091,7 @@ public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $s ], ]; if (MetaModel::HasChildrenClasses($sTargetObjectClass)) { - $aEnumChildClasses = \MetaModel::EnumChildClasses('FunctionalCI'); + $aEnumChildClasses = \MetaModel::EnumChildClasses($sTargetObjectClass); $aChildClasses = []; foreach ($aEnumChildClasses as $sClassName) { $aChildClasses[$sClassName] = MetaModel::GetName($sClassName); diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig index d48de60e86..d549af6ca4 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/object/mode_search_regular.html.twig @@ -12,7 +12,7 @@
- - {% for key, sClassName in aSubClasses %} - - {% endfor %} - +
+
+ + +
{% endif %}