diff --git a/application/utils.inc.php b/application/utils.inc.php
index 7881ccc2d..4a86debcc 100644
--- a/application/utils.inc.php
+++ b/application/utils.inc.php
@@ -90,12 +90,31 @@ class utils
* @since 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_VARIABLE_NAME = 'variable_name';
-
/**
* @var string
* @since 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_RAW_DATA = 'raw_data';
+
+ /**
+ * @var string
+ * @since 3.0.0
+ * @used-by static::GetMentionedObjectsFromText
+ */
+ public const ENUM_TEXT_FORMAT_PLAIN = 'text';
+ /**
+ * @var string
+ * @since 3.0.0
+ * @used-by static::GetMentionedObjectsFromText
+ */
+ public const ENUM_TEXT_FORMAT_HTML = 'html';
+ /**
+ * @var string
+ * @since 3.0.0
+ * @used-by static::GetMentionedObjectsFromText
+ */
+ public const ENUM_TEXT_FORMAT_MARKDOWN = 'markdown';
+
/**
* @var string
* @since 3.0.0
@@ -2867,4 +2886,56 @@ HTML;
return $sAcronym;
}
+
+ //----------------------------------------------
+ // Text manipulation
+ //----------------------------------------------
+
+ /**
+ * @param string $sText Text containing the mentioned objects to be found
+ * @param string $sFormat {@uses static::ENUM_TEXT_FORMAT_HTML, ...}
+ *
+ * @return array Array of object classes / IDs for the ones found in $sText
+ * @throws \Exception
+ * @since 3.0.0
+ */
+ public static function GetMentionedObjectsFromText(string $sText, string $sFormat = self::ENUM_TEXT_FORMAT_HTML): array
+ {
+ // First transform text so it can be parsed
+ switch ($sFormat) {
+ case static::ENUM_TEXT_FORMAT_HTML:
+ $sText = static::HtmlToText($sText);
+ break;
+
+ default:
+ // Don't transform it
+ break;
+ }
+
+ // Then parse text to find objects
+ $aMentionedObjects = array();
+ $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", $sText, $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;
+ }
+ }
+
+ return $aMentionedObjects;
+ }
}
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index e4273fd7c..0f49f6640 100644
--- a/core/dbobject.class.php
+++ b/core/dbobject.class.php
@@ -3136,39 +3136,15 @@ abstract class DBObject implements iDisplay
}
// 2 - Find mentioned objects
$aMentionedObjects = array();
- foreach($aUpdatedLogAttCodes as $sAttCode)
- {
+ 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;
- }
- }
+ $aMentionedObjects = array_merge_recursive($aMentionedObjects, utils::GetMentionedObjectsFromText($oUpdatedCaseLog->GetModifiedEntry()));
}
// 3 - Trigger for those objects
- foreach($aMentionedObjects as $sMentionedClass => $aMentionedIds)
- {
- foreach($aMentionedIds as $sMentionedId)
- {
+ // TODO: This should be refactored and moved into the caselogs loop, otherwise, we won't be able to know which case log triggered the action.
+ 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.
diff --git a/test/application/UtilsTest.php b/test/application/UtilsTest.php
index 61bbdce74..bbb3c4af3 100644
--- a/test/application/UtilsTest.php
+++ b/test/application/UtilsTest.php
@@ -440,6 +440,9 @@ class UtilsTest extends \Combodo\iTop\Test\UnitTest\ItopTestCase
$this->assertEquals($sTestedAcronym, $sExceptedAcronym, "Acronym for '$sInput' doesn't match. Got '$sTestedAcronym', expected '$sExceptedAcronym'.");
}
+ /**
+ * @since 3.0.0
+ */
public function ToAcronymProvider()
{
return [
@@ -481,4 +484,72 @@ class UtilsTest extends \Combodo\iTop\Test\UnitTest\ItopTestCase
],
];
}
+
+ /**
+ * @dataProvider GetMentionedObjectsFromTextProvider
+ * @covers utils::GetMentionedObjectsFromText
+ *
+ * @param string $sInput
+ * @param string $sFormat
+ * @param array $aExceptedMentionedObjects
+ *
+ * @throws \Exception
+ */
+ public function testGetMentionedObjectsFromText(string $sInput, string $sFormat, array $aExceptedMentionedObjects)
+ {
+ $aTestedMentionedObjects = utils::GetMentionedObjectsFromText($sInput, $sFormat);
+
+ $sExpectedAsString = print_r($aExceptedMentionedObjects, true);
+ $sTestedAsString = print_r($aTestedMentionedObjects, true);
+
+ $this->assertEquals($sTestedAsString, $sExpectedAsString, "Found mentioned objects don't match. Got: $sTestedAsString, expected $sExpectedAsString");
+ }
+
+ /**
+ * @since 3.0.0
+ */
+ public function GetMentionedObjectsFromTextProvider(): array
+ {
+ $sAbsUrlAppRoot = utils::GetAbsoluteUrlAppRoot();
+
+ return [
+ 'No object' => [
+ "Begining
+ Second line
+ End",
+ utils::ENUM_TEXT_FORMAT_HTML,
+ [],
+ ],
+ '1 UserRequest' => [
+ "Begining
+ Before link R-012345 After link
+ End",
+ utils::ENUM_TEXT_FORMAT_HTML,
+ [
+ 'UserRequest' => ['12345'],
+ ],
+ ],
+ '2 UserRequests' => [
+ "Begining
+ Before link R-012345 After link
+ And R-987654
+ End",
+ utils::ENUM_TEXT_FORMAT_HTML,
+ [
+ 'UserRequest' => ['12345', '987654'],
+ ],
+ ],
+ '1 UserRequest, 1 Person' => [
+ "Begining
+ Before link R-012345 After link
+ And Claude Monet
+ End",
+ utils::ENUM_TEXT_FORMAT_HTML,
+ [
+ 'UserRequest' => ['12345'],
+ 'Person' => ['3'],
+ ],
+ ],
+ ];
+ }
}