N°7385 - Trigger on mention executed even if it's not a mention with @ (#638)

This commit is contained in:
Anne-Catherine
2025-04-18 14:52:23 +02:00
committed by GitHub
parent 3b60e63c97
commit b21a02dcb8
4 changed files with 165 additions and 131 deletions

View File

@@ -521,7 +521,7 @@ class utils
// For URL
case static::ENUM_SANITIZATION_FILTER_URL:
// N°6350 - returns only valid URLs
// N°6350 - returns only valid URLs
$retValue = filter_var($value, FILTER_VALIDATE_URL);
break;
@@ -554,44 +554,44 @@ class utils
switch($sError)
{
case UPLOAD_ERR_OK:
$sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex];
$sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex];
$sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex];
$sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex];
$sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex];
$sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex];
$doc_content = file_get_contents($sTmpName);
$doc_content = file_get_contents($sTmpName);
$sMimeType = self::GetFileMimeType($sTmpName);
$oDocument = new ormDocument($doc_content, $sMimeType, $sName);
break;
break;
case UPLOAD_ERR_NO_FILE:
// no file to load, it's a normal case, just return an empty document
break;
// no file to load, it's a normal case, just return an empty document
break;
case UPLOAD_ERR_FORM_SIZE:
case UPLOAD_ERR_INI_SIZE:
throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize')));
break;
throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize')));
break;
case UPLOAD_ERR_PARTIAL:
throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.'));
break;
throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.'));
break;
case UPLOAD_ERR_NO_TMP_DIR:
throw new FileUploadException(Dict::S('UI:Error:NoTmpDir'));
break;
throw new FileUploadException(Dict::S('UI:Error:NoTmpDir'));
break;
case UPLOAD_ERR_CANT_WRITE:
throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir')));
break;
throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir')));
break;
case UPLOAD_ERR_EXTENSION:
$sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex];
throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName));
break;
$sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex];
throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName));
break;
default:
throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError));
break;
throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError));
break;
}
}
@@ -889,10 +889,10 @@ class utils
$aDateRegexps = array_values($aSpec);
}
$sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat);
$sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat);
if (preg_match('!^(?<head>)'.$sDateRegexp.'(?<tail>)$!', $sDate, $aMatches))
{
if (preg_match('!^(?<head>)'.$sDateRegexp.'(?<tail>)$!', $sDate, $aMatches))
{
$sYear = isset($aMatches['year']) ? $aMatches['year'] : 0;
$sMonth = isset($aMatches['month']) ? $aMatches['month'] : 1;
$sDay = isset($aMatches['day']) ? $aMatches['day'] : 1;
@@ -901,11 +901,11 @@ class utils
$sSecond = isset($aMatches['second']) ? $aMatches['second'] : 0;
return strtotime("$sYear-$sMonth-$sDay $sHour:$sMinute:$sSecond");
}
else
{
return false;
}
// http://www.spaweditor.com/scripts/regex/index.php
else
{
return false;
}
// http://www.spaweditor.com/scripts/regex/index.php
}
/**
@@ -1334,8 +1334,8 @@ class utils
return Session::GetLog();
}
static function DebugBacktrace($iLimit = 5)
{
static function DebugBacktrace($iLimit = 5)
{
$aFullTrace = debug_backtrace();
$aLightTrace = array();
for($i=1; ($i<=$iLimit && $i < count($aFullTrace)); $i++) // Skip the last function call... which is the call to this function !
@@ -1343,7 +1343,7 @@ class utils
$aLightTrace[$i] = $aFullTrace[$i]['function'].'(), called from line '.$aFullTrace[$i]['line'].' in '.$aFullTrace[$i]['file'];
}
echo "<p><pre>".print_r($aLightTrace, true)."</pre></p>\n";
}
}
/**
* Execute the given iTop PHP script, passing it the current credentials
@@ -1539,7 +1539,7 @@ class utils
if (strlen($sUrl) < SERVER_MAX_URL_LENGTH) {
// Static menus: Email this page, CSV Export & Add to Dashboard
$aResult[] = new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'),
"mailto:?body=".urlencode($sUrl).' ' // Add an extra space to make it work in Outlook
"mailto:?body=".urlencode($sUrl).' ' // Add an extra space to make it work in Outlook
);
}
@@ -1961,7 +1961,7 @@ SQL;
CURLOPT_HTTPHEADER => $aHTTPHeaders,
);
$aAllOptions = $aCurlOptions + $aOptions;
$aAllOptions = $aCurlOptions + $aOptions;
$ch = curl_init($sUrl);
curl_setopt_array($ch, $aAllOptions);
$response = curl_exec($ch);
@@ -1986,7 +1986,7 @@ SQL;
/**
* Get a standard list of character sets
*
* @param array $aAdditionalEncodings Additional values
* @param array $aAdditionalEncodings Additional values
* @return array of iconv code => english label, sorted by label
*/
public static function GetPossibleEncodings($aAdditionalEncodings = array())
@@ -2221,13 +2221,13 @@ SQL;
case 'image/gif':
case 'image/jpeg':
case 'image/png':
$img = @imagecreatefromstring($oImage->GetData());
break;
$img = @imagecreatefromstring($oImage->GetData());
break;
default:
// Unsupported image type, return the image as-is
//throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used.");
return $oImage;
// Unsupported image type, return the image as-is
//throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used.");
return $oImage;
}
if ($img === false)
{
@@ -2259,16 +2259,16 @@ SQL;
switch ($oImage->GetMimeType())
{
case 'image/gif':
imagegif($new); // send image to output buffer
break;
imagegif($new); // send image to output buffer
break;
case 'image/jpeg':
imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality
break;
imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality
break;
case 'image/png':
imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression
break;
imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression
break;
}
$oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName());
@ob_end_clean();
@@ -2298,16 +2298,16 @@ SQL;
$data .= mt_rand();
$hash = strtoupper(hash('ripemd128', $uid . md5($data)));
$sUUID = '{' .
substr($hash, 0, 8) .
'-' .
substr($hash, 8, 4) .
'-' .
substr($hash, 12, 4) .
'-' .
substr($hash, 16, 4) .
'-' .
substr($hash, 20, 12) .
'}';
substr($hash, 0, 8) .
'-' .
substr($hash, 8, 4) .
'-' .
substr($hash, 12, 4) .
'-' .
substr($hash, 16, 4) .
'-' .
substr($hash, 20, 12) .
'}';
return $sUUID;
}
@@ -2319,7 +2319,7 @@ SQL;
*/
public static function GetCurrentModuleName($iCallDepth = 0)
{
return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth + 1);
return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth + 1);
}
/**
@@ -2366,7 +2366,7 @@ SQL;
*/
public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null)
{
return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue);
return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue);
}
/**
@@ -2375,7 +2375,7 @@ SQL;
*/
public static function GetCompiledModuleVersion($sModuleName)
{
return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName);
return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName);
}
/**
@@ -3109,19 +3109,29 @@ TXT
$aMentionMatches = [];
$sText = html_entity_decode($sText);
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"/Ui', $sText, $aMentionMatches);
$aMentionAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"\s*([^>]*)>(.*)<\/a>/Ui', $sText, $aMentionMatches);
foreach ($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) {
$sMatchedClass = $aMentionMatches[2][$iMatchIdx];
$sMatchedId = $aMentionMatches[3][$iMatchIdx];
$sMatchedName = $aMentionMatches[5][$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;
}
$sMentionPrefix = array_search($sMatchedClass, $aMentionAllowedClasses);
if ($sMentionPrefix === false) {
continue;
}
//tests if the name starts with $sMentionPrefix (e.g. '@' for 'Contact' class)
if (str_starts_with($sMatchedName, $sMentionPrefix) === false) {
continue;
}
// 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;

View File

@@ -521,14 +521,13 @@ abstract class ItopTestCase extends KernelTestCase
sort($aActual);
sort($aExpected);
$sExpected = implode("\n", $aExpected);
$sActual = implode("\n", $aActual);
$sExpected = var_export($aExpected, true);
$sActual = var_export($aActual, true);
if ($sExpected === $sActual) {
$this->assertTrue(true);
return;
}
$sMessage .= "\nExpected:\n$sExpected\nActual:\n$sActual";
var_export($aActual);
$this->fail($sMessage);
}

View File

@@ -645,66 +645,6 @@ class utilsTest extends ItopTestCase
];
}
/**
* @dataProvider GetMentionedObjectsFromTextProvider
* @covers utils::GetMentionedObjectsFromText
*
* @throws \Exception
*/
public function testGetMentionedObjectsFromText($sInput, $aExceptedMentionedObjects)
{
// Emulate the "Case provider mechanism" (reason: the data provider requires utils constants not available before the application startup)
echo "testGetMentionedObjectsFromText: input = $sInput\n";
$aTestedMentionedObjects = utils::GetMentionedObjectsFromText($sInput);
$sExpectedAsString = print_r($aExceptedMentionedObjects, true);
$sTestedAsString = print_r($aTestedMentionedObjects, true);
$this->assertEquals($sExpectedAsString, $sTestedAsString, "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",
[],
],
'1 UserRequest' => [
<<<HTML
<p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="UserRequest" data-object-key="12345" data-object-id="#Test Ticket" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=UserRequest&id=12345">#Test Ticket</a>After link</p><p>End</p>
HTML,
[
'UserRequest' => ['12345'],
],
],
'2 UserRequests' => [
<<<HTML
<div class="ibo-activity-entry--main-information-content"><p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="UserRequest" data-object-key="12345" data-object-id="#Test Ticket 1" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=UserRequest&id=12345">#Test Ticket</a> After link</p><p>And <a data-role="object-mention" data-object-class="UserRequest" data-object-key="987654" data-object-id="#Test Ticket 2" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=UserRequest&id=987654">#Test Ticket</a></p><p>End</p></div>
HTML,
[
'UserRequest' => ['12345', '987654'],
],
],
'1 UserRequest, 1 Person' => [
<<<HTML
<div class="ibo-activity-entry--main-information-content"><div class="ibo-activity-entry--main-information-content"><p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="UserRequest" data-object-key="12345" data-object-id="#Test Ticket" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=UserRequest&id=12345">#Test Ticket</a> After link</p><p>And <a data-role="object-mention" data-object-class="Person" data-object-id="@Agatha Christie" data-object-key="3" data-object-id="@Agatha Christie" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=Person&id=3">@Agatha Christie</a></p><p>End</p></div></div>
HTML,
[
'UserRequest' => ['12345'],
'Person' => ['3'],
],
],
];
}
/**
* @dataProvider FormatInitialsForMedallionProvider
* @covers utils::FormatInitialsForMedallion

View File

@@ -0,0 +1,85 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
use utils;
/**
* @covers utils
*/
class utilsTestWithDataModel extends ItopDataTestCase
{
const USE_TRANSACTION = false;
/**
* @dataProvider GetMentionedObjectsFromTextProvider
* @covers utils::GetMentionedObjectsFromText
*
* @throws \Exception
*/
public function testGetMentionedObjectsFromText($sInput, $aExceptedMentionedObjects)
{
MetaModel::GetConfig()->Set('mentions.allowed_classes', ['@' => 'Person', '😊#' => 'Team']);
// Emulate the "Case provider mechanism" (reason: the data provider requires utils constants not available before the application startup)
$aTestedMentionedObjects = utils::GetMentionedObjectsFromText($sInput);
$this->AssertArraysHaveSameItems($aExceptedMentionedObjects, $aTestedMentionedObjects);
}
/**
* @since 3.0.0
*/
public function GetMentionedObjectsFromTextProvider(): array
{
$sAbsUrlAppRoot = 'https://myitop.com/itop/';
return [
'No object' => [
"Begining
Second line
End",
[],
],
'1 Object' => [
<<<HTML
<p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="Person" data-object-key="12345" data-object-id="#Test Person" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=Person&id=12345">@Test Person</a>After link</p><p>End</p>
HTML,
[
'Person' => ['12345'],
],
],
'Should not match 1 Object if the mention prefix is missing' => [
<<<HTML
<div class="ibo-activity-entry--main-information-content"><p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="Person" data-object-key="12345" data-object-id="#Test Person 1" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=Person&id=12345">#Test Ticket</a> After link</p></div>
HTML,
[],
],
'Should return 2 Objects' => [
<<<HTML
<div class="ibo-activity-entry--main-information-content"><div class="ibo-activity-entry--main-information-content"><p>Beginning</p><p>Before link <a data-role="object-mention" data-object-class="Person" data-object-key="12345" data-object-id="#Test Person" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=UserRequest&id=12345">@Test Person</a> After link</p><p>And <a data-role="object-mention" data-object-class="Person" data-object-id="@Agatha Christie" data-object-key="3" data-object-id="@Agatha Christie" href="$sAbsUrlAppRoot/pages/UI.php?operation=details&class=Person&id=3">@Agatha Christie</a></p><p>End</p></div></div>
HTML,
[
'Person' => ['12345', '3'],
],
],
'Should process objects of different classes' => [
<<<HTML
Begining
Before link <a data-object-class="Team" data-object-key="12345" href=\"$sAbsUrlAppRoot/pages/UI.php&operation=details&class=Team&id=12345&foo=bar\">😊#R-012345</a> After link
And <a data-object-class="Person" data-object-key="3" href=\"$sAbsUrlAppRoot/pages/UI.php&operation=details&class=Person&id=3&foo=bar\">@Claude Monet</a>
End
HTML,
[
'Team' => ['12345'],
'Person' => ['3'],
],
],
];
}
}