Compare commits

...

11 Commits

Author SHA1 Message Date
Stephen Abello
0d98b141bb Update core/attributedef.class.inc.php
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-07-15 14:16:31 +02:00
Stephen Abello
af61ca3311 Update core/attributedef.class.inc.php
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-07-15 12:19:50 +02:00
Molkobain
7c9390636c Apply suggestions from code review
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-07-15 12:16:13 +02:00
Stephen Abello
584d362bed Update php with right since version and ticket, remove useless method overload 2026-07-15 11:15:53 +02:00
Stephen Abello
590523e471 Define GetSize in AttributeDefinition to easy polymorphic calls instead of obscure AttributeDBFieldVoid 2026-07-15 10:50:45 +02:00
Stephen Abello
7daed4c8e6 Apply code review fixes, add tests 2026-07-15 10:31:40 +02:00
Stephen Abello
933d552e1e Fix variable types 2026-07-13 15:36:19 +02:00
Stephen Abello
5cfb15adde Fix variable types 2026-07-13 15:34:59 +02:00
Stephen Abello
39cadd64b6 Move TrimValue() base definition to AttributeDefinition class, assign DBObject::SetTrim old behavior to it 2026-07-13 15:34:09 +02:00
Anne-Cath
e08897a7ca WIP 2026-07-07 16:46:53 +02:00
Anne-Cath
89384852c1 N°9759 - Truncate AttributeText don't work as expected in case of multibytes characters 2026-07-07 11:39:01 +02:00
4 changed files with 141 additions and 39 deletions

View File

@@ -897,6 +897,37 @@ abstract class AttributeDefinition
return null;
}
/**
* @since 3.2.3-2 3.2.4 3.3.0 N°9759
*/
public function GetSize($value)
{
return mb_strlen($value);
}
/**
* Helper to set a value that fits the attribute max size
*
* Default behavior is what DBObject::SetTrim used to do, now delegated to AttributeDefinition
*
* @param string $sValue
*
* @return string
* @since 3.2.3-2 3.2.4 3.3.0 N°9759
*/
public function TrimValue(?string $sValue)
{
$iMaxSize = $this->GetMaxSize();
$iLength = mb_strlen($sValue);
if ($iMaxSize && ($iLength > $iMaxSize)) {
$sMessage = " -truncated ($iLength chars)";
return mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
return $sValue;
}
/**
* @return mixed|null
* @deprecated never used
@@ -4219,6 +4250,37 @@ class AttributeText extends AttributeString
return "TEXT".CMDBSource::GetSqlStringColumnDefinition();
}
/**
* @inheritDoc
*/
public function GetSize($value)
{
return strlen($value);
}
/**
* @inheritDoc
*/
public function TrimValue(?string $sValue)
{
$iMaxSize = $this->GetMaxSize();
$iLength = strlen($sValue);
$iLengthChar = mb_strlen($sValue);
if ($iMaxSize && ($iLength > $iMaxSize)) {
$sMessage = " -truncated ($iLengthChar chars)";
$iTruncatedValueMaxSize = $iMaxSize - strlen($sMessage);
$sTruncatedValue = substr($sValue, 0, $iTruncatedValueMaxSize);
// Keep trimming bytes until we have valid UTF-8 to avoid returning a broken multibyte sequence.
while (($sTruncatedValue !== '') && !mb_check_encoding($sTruncatedValue, 'UTF-8')) {
$sTruncatedValue = substr($sTruncatedValue, 0, -1);
}
return $sTruncatedValue.$sMessage;
}
return $sValue;
}
public function GetSQLColumns($bFullSpec = false)
{
$aColumns = [];

View File

@@ -732,13 +732,8 @@ abstract class DBObject implements iDisplay
public function SetTrim($sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$iMaxSize = $oAttDef->GetMaxSize();
$sLength = mb_strlen($sValue);
if ($iMaxSize && ($sLength > $iMaxSize)) {
$sMessage = " -truncated ($sLength chars)";
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
}
$this->Set($sAttCode, $sValue);
$this->Set($sAttCode, $oAttDef->TrimValue($sValue));
}
/**
@@ -2040,7 +2035,7 @@ abstract class DBObject implements iDisplay
}
}
if (!is_null($iMaxSize = $oAtt->GetMaxSize())) {
$iLen = mb_strlen($toCheck);
$iLen = $oAtt->GetSize($toCheck);
if ($iLen > $iMaxSize) {
return "String too long (found $iLen, limited to $iMaxSize)";
}

View File

@@ -37,7 +37,6 @@ use Team;
use User;
use UserRequest;
use UserRights;
use utils;
/**
* @group specificOrgInSampleData
@@ -1297,19 +1296,21 @@ class DBObjectTest extends ItopDataTestCase
{
return [
// UserRequest.title is an AttributeString (maxsize = 255)
'title 250 chars' => ['title', 250],
'title 254 chars' => ['title', 254],
'title 255 chars' => ['title', 255],
'title 256 chars' => ['title', 256],
'title 300 chars' => ['title', 300],
'title 250 chars' => ['title', 250, 250, true],
'title 254 chars' => ['title', 254, 254, true],
'title 255 chars' => ['title', 255, 255, true],
'title 256 chars' => ['title', 256, 255, false],
'title 300 chars' => ['title', 300, 255, false],
// UserRequest.pending_reason is an AttributeText (maxsize=65535) with format=text
'pending_reason 250 chars' => ['pending_reason', 250],
'pending_reason 60000 chars' => ['pending_reason', 60000],
'pending_reason 65534 chars' => ['pending_reason', 65534],
'pending_reason 65535 chars' => ['pending_reason', 65535],
'pending_reason 65536 chars' => ['pending_reason', 65536],
'pending_reason 70000 chars' => ['pending_reason', 70000],
'pending_reason 250 chars' => ['pending_reason', 250, 250, true],
'pending_reason 65534 chars' => ['pending_reason', 65534, 16403, false],
'pending_reason 65535 chars' => ['pending_reason', 65535, 16403, false],
'pending_reason 65536 chars' => ['pending_reason', 65536, 16403, false],
'pending_reason 16385 chars' => ['pending_reason', 16385, 16403, false],
'pending_reason 16384 chars' => ['pending_reason', 16384, 16384, true],
'pending_reason 16383 chars' => ['pending_reason', 16383, 16383, true],
'pending_reason 16382 chars' => ['pending_reason', 16382, 16382, true],
];
}
@@ -1322,7 +1323,7 @@ class DBObjectTest extends ItopDataTestCase
*
* @since 3.1.2 N°3448 - Framework field size check not correctly implemented for multi-bytes languages/strings
*/
public function testCheckLongValueInAttribute(string $sAttrCode, int $iValueLength)
public function testCheckLongValueInAttribute(string $sAttrCode, int $iValueLength, int $iExpectedLength, bool $bIsValueToSetBelowAttrMaxSize): void
{
$sPrefix = 'a'; // just a small prefix so that the emoji bytes won't have a power of 2 (we want a non even value)
$sEmojiToRepeat = '😎'; // this emoji is 4 bytes long
@@ -1343,17 +1344,18 @@ class DBObjectTest extends ItopDataTestCase
$oAttDef = MetaModel::GetAttributeDef(UserRequest::class, $sAttrCode);
$iAttrMaxSize = $oAttDef->GetMaxSize();
$bIsValueToSetBelowAttrMaxSize = ($iValueLength <= $iAttrMaxSize);
$bExpectedStatus = ($oAttDef->GetSize($sValueToSet) <= $iAttrMaxSize);
$this->assertSame($bExpectedStatus, $bIsValueToSetBelowAttrMaxSize, 'The data provider must stay aligned with the attribute max size logic.');
/** @noinspection PhpUnusedLocalVariableInspection */
[$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oTicket->CheckToWrite();
$this->assertEquals($bIsValueToSetBelowAttrMaxSize, $bCheckStatus, "CheckResult result:".var_export($aCheckIssues, true));
$oTicket->SetTrim($sAttrCode, $sValueToSet);
$sValueInObject = $oTicket->Get($sAttrCode);
$this->assertEquals($iExpectedLength, mb_strlen($sValueInObject), 'Should match expected resulting value length.');
if ($bIsValueToSetBelowAttrMaxSize) {
$this->assertEquals($sValueToSet, $sValueInObject, 'Should not alter string that is already shorter than attribute max length');
} else {
$this->assertEquals($iAttrMaxSize, mb_strlen($sValueInObject), 'Should truncate at the same length than attribute max length');
$sLastCharsOfValueInObject = mb_substr($sValueInObject, -30);
$this->assertStringContainsString(' -truncated', $sLastCharsOfValueInObject, 'Should end with "truncated" comment');
}
@@ -1386,6 +1388,61 @@ class DBObjectTest extends ItopDataTestCase
$this->assertEquals($sResult, $oOrganisation->Get('name'), 'SetTrim must limit string to 255 characters');
}
/**
* Check that DBObject::SetTrim doesn't cut through multibytes characters
*
* @covers DBObject::SetTrim
* @dataProvider SetTrimAttributeTextProvider
*/
public function testSetTrimOnAttributeTextKeepsUtf8Validity(string $sChar, int $iRepeatCount, bool $bExpectExactByteFill): void
{
$oTicket = MetaModel::NewObject('UserRequest', [
'ref' => 'Test Ticket',
'title' => 'Create OK',
'description' => 'Create OK',
'caller_id' => 15,
'org_id' => 3,
]);
$sValueToSet = str_repeat($sChar, $iRepeatCount);
$oTicket->SetTrim('pending_reason', $sValueToSet);
$sValueInObject = $oTicket->Get('pending_reason');
$oAttDef = MetaModel::GetAttributeDef('UserRequest', 'pending_reason');
$iAttrMaxSize = $oAttDef->GetMaxSize();
$iOriginalCharLength = mb_strlen($sValueToSet);
$sMessage = " -truncated ($iOriginalCharLength chars)";
$this->assertStringEndsWith($sMessage, $sValueInObject, 'Trimmed value should keep the expected truncation suffix.');
$this->assertTrue(mb_check_encoding($sValueInObject, 'UTF-8'), 'Trimmed value should stay valid UTF-8.');
$this->assertLessThanOrEqual($iAttrMaxSize, strlen($sValueInObject), 'Trimmed value should never exceed attribute byte max size.');
if ($bExpectExactByteFill) {
$this->assertSame($iAttrMaxSize, strlen($sValueInObject), 'When byte cut lands on a character boundary, SetTrim should use all available bytes.');
}
}
public function SetTrimAttributeTextProvider()
{
return [
// 2-byte UTF-8 chars: truncation payload size is byte-aligned and should fill the max size exactly.
'pending_reason 2-byte chars on byte boundary' => ["\xC3\xA9", 32768, true],
// 4-byte UTF-8 chars: truncation payload size is not byte-aligned and must backtrack to valid UTF-8.
'pending_reason 4-byte chars with mid-character byte cut' => ['💃', 16385, false],
];
}
/**
* @covers DBObject::SetTrim
*/
public function testSetTrimOnNonStringAttributeDoesNotTrim()
{
$oTicket = MetaModel::NewObject(UserRequest::class);
$oTicket->SetTrim('caller_id', '15');
$this->assertEquals(15, $oTicket->Get('caller_id'), 'SetTrim should keep non-string attributes untouched before regular Set conversion');
}
/**
* @covers DBObject::SetComputedDate
* @return void

View File

@@ -277,25 +277,13 @@ abstract class WebServicesBase
$oLog->Set('userinfo', UserRights::GetUser());
$oLog->Set('verb', $sVerb);
$oLog->Set('result', $oRes->IsOk());
$this->TrimAndSetValue($oLog, 'log_info', (string)$oRes->GetInfoAsText());
$this->TrimAndSetValue($oLog, 'log_warning', (string)$oRes->GetWarningsAsText());
$this->TrimAndSetValue($oLog, 'log_error', (string)$oRes->GetErrorsAsText());
$this->TrimAndSetValue($oLog, 'data', (string)$oRes->GetReturnedDataAsText());
$oLog->SetTrim('log_info', (string)$oRes->GetInfoAsText());
$oLog->SetTrim('log_warning', (string)$oRes->GetWarningsAsText());
$oLog->SetTrim('log_error', (string)$oRes->GetErrorsAsText());
$oLog->SetTrim('data', (string)$oRes->GetReturnedDataAsText());
$oLog->DBInsertNoReload();
}
protected function TrimAndSetValue($oLog, $sAttCode, $sValue)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($oLog), $sAttCode);
if (is_object($oAttDef)) {
$iMaxSize = $oAttDef->GetMaxSize();
if ($iMaxSize && (mb_strlen($sValue) > $iMaxSize)) {
$sValue = mb_substr($sValue, 0, $iMaxSize);
}
$oLog->Set($sAttCode, $sValue);
}
}
/**
* Helper to set a scalar attribute
*