diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 80a05cb4e..ec0d79d4e 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -1969,6 +1969,7 @@ EOF ); break; + // TODO 3.0.0: Isn't this part obsolete now that we have the activity panel or should we keep it for devs using it in custom extensions? case 'CaseLog': $aStyles = array(); $sStyle = ''; @@ -2014,6 +2015,8 @@ EOF // b) or override some of the configuration settings, using the second parameter of ckeditor() $aConfig = utils::GetCkeditorPref(); $aConfig['placeholder'] = Dict::S('UI:CaseLogTypeYourTextHere'); + + // - Final config $sConfigJS = json_encode($aConfig); $oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, $sConfigJS);"); // Transform $iId into a CKEdit @@ -2358,7 +2361,7 @@ EOF EOF ); } - + $sHTMLValue = '
'.$sValidationSpan.$sReloadSpan; $aEventsList[] = 'keyup'; diff --git a/application/utils.inc.php b/application/utils.inc.php index 624677ae4..56c82996a 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -2482,14 +2482,55 @@ class utils return static::$iNextId++; } + /** + * Return the CKEditor config as an array + * + * @return array + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + * @since 3.0.0 + */ public static function GetCkeditorPref() { $sLanguage = strtolower(trim(UserRights::GetUserLanguage())); - $aDefaultConf = array('language'=> $sLanguage, + $aDefaultConf = array( + 'language'=> $sLanguage, 'contentsLanguage' => $sLanguage, - 'extraPlugins' => 'disabler,codesnippet', + 'extraPlugins' => 'disabler,codesnippet,mentions', ); + + // Mentions + $aMentionsAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes'); + if(!empty($aMentionsAllowedClasses)) { + $aDefaultConf['mentions'] = []; + + foreach($aMentionsAllowedClasses as $sMentionChar => $sMentionClass) { + // Note: Endpoints are defaults only and should be overloaded by other GUIs such as the end-users portal + $sMentionEndpoint = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=cke_mentions&target_class='.$sMentionClass.'&needle={encodedQuery}'; + $sMentionItemUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class='.$sMentionClass.'&id={id}'; + + $sMentionItemPictureTemplate = (empty(MetaModel::GetImageAttributeCode($sMentionClass))) ? '' : << +HTML; + $sMentionItemTemplate = <<{$sMentionItemPictureTemplate}{friendlyname} +HTML; + $sMentionOutputTemplate = <<{$sMentionChar}{friendlyname} +HTML; + + $aDefaultConf['mentions'][] = [ + 'feed' => $sMentionEndpoint, + 'marker' => $sMentionChar, + 'minChars' => MetaModel::GetConfig()->Get('min_autocomplete_chars'), + 'itemTemplate' => $sMentionItemTemplate, + 'outputTemplate' => $sMentionOutputTemplate, + 'throttle' => 500, + ]; + } + } $aRichTextConfig = json_decode(appUserPreferences::GetPref('richtext_config', '{}'), true); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 8fd45b9f0..63d62a9ef 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1152,6 +1152,16 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ], + 'mentions.allowed_classes' => [ + 'type' => 'array', + 'description' => 'Classes which can be mentioned through the autocomplete in the caselogs. Key of the array must be a single character that will trigger the autocomplete (eg. "@" => "Person")', + 'default' => [ + '@' => 'Person', + ], + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ], 'global_search.enabled' => [ 'type' => 'bool', 'description' => 'Whether or not the global search is enabled', diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 6ab87299e..a8cdc03fd 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1,20 +1,21 @@ +/** + * Copyright (C) 2013-2019 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ /** * All objects to be displayed in the application (either as a list or as details) @@ -3123,6 +3124,7 @@ abstract class DBObject implements iDisplay $aOriginalValues = $this->m_aOrigValues; // Activate any existing trigger + // - TriggerOnObjectUpdate $sClass = get_class($this); $aParams = array('class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)"), @@ -3139,6 +3141,68 @@ abstract class DBObject implements iDisplay utils::EnrichRaisedException($oTrigger, $e); } } + // - TriggerOnObjectMention + // 1 - Check if any caselog updated + $aUpdatedLogAttCodes = array(); + foreach($aChanges as $sAttCode => $value) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if($oAttDef instanceof AttributeCaseLog) + { + $aUpdatedLogAttCodes[] = $sAttCode; + } + } + // 2 - Find mentioned objects + $aMentionedObjects = array(); + foreach($aUpdatedLogAttCodes as $sAttCode) + { + /** @var \ormCaseLog $oUpdatedCaseLog */ + $oUpdatedCaseLog = $this->Get($sAttCode); + $aMentionMatches = array(); + // Note: As the sanitizer (or CKEditor autocomplete plugin? 🤔) removes data-* attributes from the hyperlink, we can't use the following (simpler) regexp: '/]*)data-object-class="([^"]*)"\s*data-object-id="([^"]*)">/i' + // If we change the sanitizer, we might want to use this regexp as it's easier to read + // Note 2: This is only working for backoffice URLs... + $sAppRootUrlForRegExp = addcslashes(utils::GetAbsoluteUrlAppRoot(), '/&'); + preg_match_all("/\[([^\]]*)\]\({$sAppRootUrlForRegExp}[^\)]*\&class=([^\)\&]*)\&id=([\d]*)[^\)]*\)/i", $oUpdatedCaseLog->GetModifiedEntry(), $aMentionMatches); + + foreach($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) + { + $sMatchedClass = $aMentionMatches[2][$iMatchIdx]; + $sMatchedId = $aMentionMatches[3][$iMatchIdx]; + + // Prepare array for matched class if not already present + if(!array_key_exists($sMatchedClass, $aMentionedObjects)) + { + $aMentionedObjects[$sMatchedClass] = array(); + } + // Add matched ID if not already there + if(!in_array($sMatchedId, $aMentionedObjects[$sMatchedClass])) + { + $aMentionedObjects[$sMatchedClass][] = $sMatchedId; + } + } + } + // 3 - Trigger for those objects + foreach($aMentionedObjects as $sMentionedClass => $aMentionedIds) + { + foreach($aMentionedIds as $sMentionedId) + { + /** @var \DBObject $oMentionedObject */ + $oMentionedObject = MetaModel::GetObject($sMentionedClass, $sMentionedId); + // Important: Here the "$this->object()$" placeholder is actually the mentioned object and not the current object. The current object can be used through the $source->object()$ placeholder. + // This is due to the current implementation of triggers, the events will only be visible on the object the trigger's OQL is based on... 😕 + $aTriggerArgs = $this->ToArgs('source') + array('this->object()' => $oMentionedObject); + + $aParams = array('class_list' => MetaModel::EnumParentClasses($sMentionedClass, ENUM_PARENT_CLASSES_ALL)); + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), + array(), $aParams); + while ($oTrigger = $oSet->Fetch()) + { + /** @var \Trigger $oTrigger */ + $oTrigger->DoActivate($aTriggerArgs); + } + } + } $bHasANewExternalKeyValue = false; $aHierarchicalKeys = array(); diff --git a/core/htmlsanitizer.class.inc.php b/core/htmlsanitizer.class.inc.php index dbda6e59a..ccda5bd61 100644 --- a/core/htmlsanitizer.class.inc.php +++ b/core/htmlsanitizer.class.inc.php @@ -162,7 +162,7 @@ class HTMLDOMSanitizer extends HTMLSanitizer protected static $aTagsWhiteList = array( 'html' => array(), 'body' => array(), - 'a' => array('href', 'name', 'style', 'target', 'title'), + 'a' => array('href', 'name', 'style', 'target', 'title', 'data-role', 'data-object-class', 'data-object-id'), 'p' => array('style'), 'blockquote' => array('style'), 'br' => array(), diff --git a/core/trigger.class.inc.php b/core/trigger.class.inc.php index a89359dbe..aa718d537 100644 --- a/core/trigger.class.inc.php +++ b/core/trigger.class.inc.php @@ -1,31 +1,22 @@ - - /** - * Persistent class Trigger and derived - * User defined triggers, that may be used in conjunction with user defined actions + * Copyright (C) 2013-2019 Combodo SARL * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License */ - /** * A user defined trigger, to customize the application * A trigger will activate an action @@ -563,6 +554,69 @@ class TriggerOnObjectUpdate extends TriggerOnObject } +/** + * Class TriggerOnObjectMention + * + * @author Guillaume Lajarige