From c3582f0affc3cdbb05fe1c0d064fab05a73ac513 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Thu, 4 Jul 2024 11:30:10 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B07552=20-=20Fix=20mentions=20not=20taking?= =?UTF-8?q?=20triggers=20filter=20into=20account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/ckeditor.feeds.js | 4 +- pages/ajax.render.php | 120 +----------------- sources/Application/Helper/CKEditorHelper.php | 2 +- .../CaseLogEntryForm/CaseLogEntryForm.php | 19 ++- .../Base/Layout/ObjectController.php | 93 ++++++++++++++ sources/Service/Base/ObjectRepository.php | 9 +- 6 files changed, 125 insertions(+), 122 deletions(-) diff --git a/js/ckeditor.feeds.js b/js/ckeditor.feeds.js index 0eed695d4..558e86aa4 100644 --- a/js/ckeditor.feeds.js +++ b/js/ckeditor.feeds.js @@ -22,12 +22,12 @@ const CombodoCKEditorFeeds = { }) .then(json => { // ckeditor mandatory data - json.data['search_data'].forEach(e => { + json.search_data.forEach(e => { e['name'] = e['friendlyname']; e['id'] = options['marker']+e['friendlyname']; }); // return searched data - resolve( json.data['search_data']); + resolve( json.search_data); }); }, options.throttle); diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 20c1cd6e2..cdc6ed561 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -2334,121 +2334,13 @@ EOF $oPage->add(""); break; - // TODO 3.0.0: Move this to new ajax render controller? + /** + * @internal + * @deprecated 3.2.0 N°7552 Use object.search_for_mentions route instead + */ case 'cke_mentions': - $oPage->SetContentType('application/json'); - $sMarker = utils::ReadParam('marker', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); - $sNeedle = utils::ReadParam('needle', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); - $sHostClass = utils::ReadParam('host_class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS); - $iHostId = (int)utils::ReadParam('host_id', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER); - - // Check parameters - if ($sMarker === '') { - throw new Exception('Invalid parameters, marker must be specified.'); - } - - $aMentionsAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes'); - if (isset($aMentionsAllowedClasses[$sMarker]) === false) { - throw new Exception('Invalid marker "'.$sMarker.'"'); - } - - $aMatches = array(); - if ($sNeedle !== '') { - // Retrieve mentioned class from marker - $sMentionedClass = $aMentionsAllowedClasses[$sMarker]; - if (MetaModel::IsValidClass($sMentionedClass) === false) { - throw new Exception('Invalid class "'.$sMentionedClass.'" for marker "'.$sMarker.'"'); - } - - // Base search used when no trigger configured - $oSearch = DBSearch::FromOQL("SELECT $sMentionedClass"); - $aSearchParams = ['needle' => "%$sNeedle%"]; - - // Retrieve restricting scopes from triggers if any - if ((strlen($sHostClass) > 0) && ($iHostId > 0)) { - $oHostObj = MetaModel::GetObject($sHostClass, $iHostId); - $aSearchParams['this'] = $oHostObj; - - $aTriggerMentionedSearches = []; - - $aTriggerSetParams = array('class_list' => MetaModel::EnumParentClasses($sHostClass, ENUM_PARENT_CLASSES_ALL)); - $oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), array(), $aTriggerSetParams); - /** @var \TriggerOnObjectMention $oTrigger */ - while ($oTrigger = $oTriggerSet->Fetch()) { - $sTriggerMentionedOQL = $oTrigger->Get('mentioned_filter'); - - // No filter on mentioned objects, don't restrict the scope at all, it can be any object of $sMentionedClass - if (strlen($sTriggerMentionedOQL) === 0) { - $aTriggerMentionedSearches = [$oSearch]; - break; - } - - $oTriggerMentionedSearch = DBSearch::FromOQL($sTriggerMentionedOQL); - $sTriggerMentionedClass = $oTriggerMentionedSearch->GetClass(); - - // Filter is not about the mentioned class, don't mind it - if (is_a($sMentionedClass, $sTriggerMentionedClass, true) === false) { - continue; - } - - $aTriggerMentionedSearches[] = $oTriggerMentionedSearch; - } - - if (count($aTriggerMentionedSearches) > 0) { - $oSearch = new DBUnionSearch($aTriggerMentionedSearches); - } - } - - $sSearchMainClassName = $oSearch->GetClass(); - $sSearchMainClassAlias = $oSearch->GetClassAlias(); - - $sObjectImageAttCode = MetaModel::GetImageAttributeCode($sSearchMainClassName); - - // Add condition to filter on the friendlyname - $oSearch->AddConditionExpression( - new BinaryExpression(new FieldExpression('friendlyname', $sSearchMainClassAlias), 'LIKE', new VariableExpression('needle')) - ); - - $oSet = new DBObjectSet($oSearch, [], $aSearchParams); - // Optimize fields to load - $aObjectAttCodesToLoad = []; - if (MetaModel::IsValidAttCode($sSearchMainClassName, $sObjectImageAttCode)) { - $aObjectAttCodesToLoad[] = $sObjectImageAttCode; - } - $oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aObjectAttCodesToLoad]); - $oSet->SetLimit(MetaModel::GetConfig()->Get('max_autocomplete_results')); - // Note: We have to this manually because of a bug in DBSearch not checking the user prefs. by default. - $oSet->SetShowObsoleteData(utils::ShowObsoleteData()); - - while ($oObject = $oSet->Fetch()) { - // Note $oObject finalclass might be different than $sMentionedClass - $sObjectClass = get_class($oObject); - $iObjectId = $oObject->GetKey(); - $aMatch = [ - 'class' => $sObjectClass, - 'id' => $iObjectId, - 'friendlyname' => $oObject->Get('friendlyname'), - ]; - - // Try to retrieve image for contact - if (!empty($sObjectImageAttCode)) { - /** @var \ormDocument $oImage */ - $oImage = $oObject->Get($sObjectImageAttCode); - if (!$oImage->IsEmpty()) { - $aMatch['picture_style'] = "background-image: url('".$oImage->GetDisplayURL($sObjectClass, $iObjectId, $sObjectImageAttCode)."')"; - $aMatch['initials'] = ''; - } else { - // If no image found, fallback on initials - $aMatch['picture_style'] = ''; - $aMatch['initials'] = utils::FormatInitialsForMedallion(utils::ToAcronym($oObject->Get('friendlyname'))); - } - } - - $aMatches[] = $aMatch; - } - } - - $oPage->add(json_encode($aMatches)); + $oController = new ObjectController(); + $oPage = $oController->OperationSearchForMentions(); break; case 'custom_fields_update': diff --git a/sources/Application/Helper/CKEditorHelper.php b/sources/Application/Helper/CKEditorHelper.php index 3209cc81b..4848b485f 100644 --- a/sources/Application/Helper/CKEditorHelper.php +++ b/sources/Application/Helper/CKEditorHelper.php @@ -107,7 +107,7 @@ class CKEditorHelper 'minimumCharacters' => MetaModel::GetConfig()->Get('min_autocomplete_chars'), 'feed_type' => 'ajax', 'feed_ajax_options' => [ - 'url' => utils::GetAbsoluteUrlAppRoot(). "pages/ajax.render.php?route=object.search&object_class=$sMentionClass&oql=SELECT $sMentionClass&search=", + 'url' => utils::GetAbsoluteUrlAppRoot(). "pages/ajax.render.php?route=object.search_for_mentions&marker=".urlencode($sMentionMarker)."&needle=", 'throttle' => 500, 'marker' => $sMentionMarker, ], diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php b/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php index 5bb259bed..957e573ef 100644 --- a/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php +++ b/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php @@ -217,12 +217,23 @@ class CaseLogEntryForm extends UIContentBlock $this->oTextInput = new RichText(); // Add the "host_class" to the mention endpoints so it can filter objects regarding the triggers + // Mind that `&needle=` must be ending the endpoint URL in order for the JS plugin to append the needle string $aConfig = $this->oTextInput->GetConfig(); - if (isset($aConfig['mentions'])) { - foreach ($aConfig['mentions'] as $iIdx => $aData) { - $sFeed = $aConfig['mentions'][$iIdx]['feed']; + if (isset($aConfig['mention']['feeds'])) { + foreach ($aConfig['mention']['feeds'] as $iIdx => $aData) { + $sFeed = $aConfig['mention']['feeds'][$iIdx]['feed_ajax_options']['url']; + + // Remove existing "needle" parameter + $sFeed = str_replace('&needle=', '', $sFeed); + + // Add new parameters $sFeed = utils::AddParameterToUrl($sFeed, 'host_class', $this->GetObjectClass()); - $aConfig['mentions'][$iIdx]['feed'] = utils::AddParameterToUrl($sFeed, 'host_id', $this->GetObjectId()); + $sFeed = utils::AddParameterToUrl($sFeed, 'host_id', $this->GetObjectId()); + + // Re-append "needle" parameter + $sFeed = utils::AddParameterToUrl($sFeed, 'needle', ''); + + $aConfig['mention']['feeds'][$iIdx]['feed_ajax_options']['url'] = $sFeed; } } $this->oTextInput->SetConfig($aConfig); diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php index b9f22f467..0bffc9571 100644 --- a/sources/Controller/Base/Layout/ObjectController.php +++ b/sources/Controller/Base/Layout/ObjectController.php @@ -21,6 +21,10 @@ use Combodo\iTop\Controller\AbstractController; use Combodo\iTop\Service\Base\ObjectRepository; use Combodo\iTop\Service\Router\Router; use CoreCannotSaveObjectException; +use DBObjectSearch; +use DBObjectSet; +use DBSearch; +use DBUnionSearch; use DeleteException; use Dict; use Exception; @@ -833,6 +837,95 @@ JS; ]); } + public function OperationSearchForMentions(): JsonPage + { + $oPage = new JsonPage(); + $oPage->SetOutputDataOnly(true); + + $sMarker = utils::ReadParam('marker', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + $sNeedle = utils::ReadParam('needle', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + $sHostClass = utils::ReadParam('host_class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS); + $iHostId = (int) utils::ReadParam('host_id', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER); + + // Check parameters + if (utils::IsNullOrEmptyString($sMarker)) { + throw new ApplicationException('Invalid parameters, marker must be specified.'); + } + if (utils::IsNullOrEmptyString($sNeedle)) { + throw new ApplicationException('Invalid parameters, needle must be specified.'); + } + + $aMentionsAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes'); + if (isset($aMentionsAllowedClasses[$sMarker]) === false) { + throw new ApplicationException('Invalid marker "'.$sMarker.'"'); + } + + $aMatches = array(); + // Retrieve mentioned class from marker + $sMentionedClass = $aMentionsAllowedClasses[$sMarker]; + if (MetaModel::IsValidClass($sMentionedClass) === false) { + throw new ApplicationException('Invalid class "'.$sMentionedClass.'" for marker "'.$sMarker.'"'); + } + + // Base search used when no trigger configured + $oSearch = DBSearch::FromOQL("SELECT $sMentionedClass"); + $aSearchParams = ['needle' => "%$sNeedle%"]; + + // Retrieve restricting scopes from triggers if any + if (utils::IsNotNullOrEmptyString($sHostClass) && ($iHostId > 0)) { + $oHostObj = MetaModel::GetObject($sHostClass, $iHostId); + $aSearchParams['this'] = $oHostObj; + + $aTriggerMentionedSearches = []; + + $aTriggerSetParams = array('class_list' => MetaModel::EnumParentClasses($sHostClass, ENUM_PARENT_CLASSES_ALL)); + $oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), array(), $aTriggerSetParams); + /** @var \TriggerOnObjectMention $oTrigger */ + while ($oTrigger = $oTriggerSet->Fetch()) { + $sTriggerMentionedOQL = $oTrigger->Get('mentioned_filter'); + + // No filter on mentioned objects, don't restrict the scope at all, it can be any object of $sMentionedClass + if (utils::IsNullOrEmptyString($sTriggerMentionedOQL)) { + $aTriggerMentionedSearches = [$oSearch]; + break; + } + + $oTriggerMentionedSearch = DBSearch::FromOQL($sTriggerMentionedOQL); + $sTriggerMentionedClass = $oTriggerMentionedSearch->GetClass(); + + // Filter is not about the mentioned class, don't mind it + if (is_a($sMentionedClass, $sTriggerMentionedClass, true) === false) { + continue; + } + + $aTriggerMentionedSearches[] = $oTriggerMentionedSearch; + } + + if (count($aTriggerMentionedSearches) > 0) { + $oSearch = new DBUnionSearch($aTriggerMentionedSearches); + } + } + + $sSearchMainClassName = $oSearch->GetClass(); + $sSearchMainClassAlias = $oSearch->GetClassAlias(); + + $sObjectImageAttCode = MetaModel::GetImageAttributeCode($sSearchMainClassName); + + + // Optimize fields to load + $aObjectAttCodesToLoad = []; + if (MetaModel::IsValidAttCode($sSearchMainClassName, $sObjectImageAttCode)) { + $aObjectAttCodesToLoad[] = $sObjectImageAttCode; + } + + $aResult = ObjectRepository::SearchFromOql($sSearchMainClassName, $aObjectAttCodesToLoad, $oSearch->ToOQL(), $sNeedle, $oHostObj, MetaModel::GetConfig()->Get('max_autocomplete_results')); + + return $oPage->SetData([ + 'search_data' => $aResult, + 'success' => $aResult !== null, + ]); + } + /** * OperationGet. * diff --git a/sources/Service/Base/ObjectRepository.php b/sources/Service/Base/ObjectRepository.php index 7ad5c93cd..14a8b1637 100644 --- a/sources/Service/Base/ObjectRepository.php +++ b/sources/Service/Base/ObjectRepository.php @@ -79,10 +79,12 @@ class ObjectRepository * @param string $sOql Oql expression * @param string $sSearch Friendly name search string * @param DBObject|null $oThisObject This object reference for oql + * @param int $iLimit Limit results to the $iLimit first elements * * @return array|null + * @since 3.2.0 Add $iLimit parameter */ - public static function SearchFromOql(string $sObjectClass, array $aFieldsToLoad, string $sOql, string $sSearch, DBObject $oThisObject = null): ?array + public static function SearchFromOql(string $sObjectClass, array $aFieldsToLoad, string $sOql, string $sSearch, DBObject $oThisObject = null, int $iLimit = 0): ?array { try { @@ -94,6 +96,11 @@ class ObjectRepository // Create db set from db search $oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], ['this' => $oThisObject]); + // Limit results + if ($iLimit > 0) { + $oDbObjectSet->SetLimit($iLimit); + } + // return object array return ObjectRepository::DBSetToObjectArray($oDbObjectSet, $sObjectClass, $aFieldsToLoad); }