From cff50f8732beb11b35c51cd8f6b105d697bbd449 Mon Sep 17 00:00:00 2001 From: Anne-Catherine <57360138+accognet@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:10:23 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B03448=20-=20Framework=20field=20size=20ch?= =?UTF-8?q?eck=20not=20correctly=20implemented=20for=20multibytes=20langua?= =?UTF-8?q?ges/strings=20(#528)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/dbobject.class.php | 32 +++--- core/displayablegraph.class.inc.php | 4 +- core/event.class.inc.php | 41 +++----- synchro/synchrodatasource.class.inc.php | 29 ++---- .../unitary-tests/core/DBObjectTest.php | 97 ++++++++++++++++++- .../unitary-tests/core/EventIssueTest.php | 69 +++++++++++++ webservices/webservices.class.inc.php | 5 +- 7 files changed, 207 insertions(+), 70 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/core/EventIssueTest.php diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 3c9042048..fd4da584b 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -756,9 +756,10 @@ abstract class DBObject implements iDisplay { $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); $iMaxSize = $oAttDef->GetMaxSize(); - if ($iMaxSize && (strlen($sValue) > $iMaxSize)) - { - $sValue = substr($sValue, 0, $iMaxSize); + $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); } @@ -2110,33 +2111,26 @@ abstract class DBObject implements iDisplay return true; } - if ($toCheck instanceof ormSet) - { + if ($toCheck instanceof ormSet) { return true; } return "Bad type"; - } - elseif ($oAtt->IsScalar()) - { + } elseif ($oAtt->IsScalar()) { + $aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery()); - if (is_array($aValues) && (count($aValues) > 0)) - { - if (!array_key_exists($toCheck, $aValues)) - { + if (is_array($aValues) && (count($aValues) > 0)) { + if (!array_key_exists($toCheck, $aValues)) { return "Value not allowed [$toCheck]"; } } - if (!is_null($iMaxSize = $oAtt->GetMaxSize())) - { - $iLen = strlen($toCheck); - if ($iLen > $iMaxSize) - { + if (!is_null($iMaxSize = $oAtt->GetMaxSize())) { + $iLen = mb_strlen($toCheck); + if ($iLen > $iMaxSize) { return "String too long (found $iLen, limited to $iMaxSize)"; } } - if (!$oAtt->CheckFormat($toCheck)) - { + if (!$oAtt->CheckFormat($toCheck)) { return "Wrong format [$toCheck]"; } } diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php index f1fdd6e76..1291bebee 100644 --- a/core/displayablegraph.class.inc.php +++ b/core/displayablegraph.class.inc.php @@ -62,7 +62,7 @@ class DisplayableNode extends GraphNode public function GetWidth() { - return max(32, 5*strlen($this->GetProperty('label'))); // approximation of the text's bounding box + return max(32, 5 * mb_strlen($this->GetProperty('label'))); // approximation of the text's bounding box } public function GetHeight() @@ -491,7 +491,7 @@ class DisplayableNode extends GraphNode if ($bNoLabel) { // simulate a fake label with the approximate same size as the true label - $sLabel = str_repeat('x',strlen($this->GetProperty('label', $this->GetId()))); + $sLabel = str_repeat('x', mb_strlen($this->GetProperty('label', $this->GetId()))); $sDot = 'label="'.$sLabel.'"'; } else diff --git a/core/event.class.inc.php b/core/event.class.inc.php index da6f0522a..35f41c91f 100644 --- a/core/event.class.inc.php +++ b/core/event.class.inc.php @@ -242,44 +242,33 @@ class EventIssue extends Event { if (is_string($sValue)) { - if (strlen($sValue) < 256) - { + if (mb_strlen($sValue) < 256) { $aPost[$sKey] = $sValue; + } else { + $aPost[$sKey] = "!long string: ".mb_strlen($sValue)." chars"; } - else - { - $aPost[$sKey] = "!long string: ".strlen($sValue). " chars"; - } - } - else - { + } else { // Not a string (avoid warnings in case the value cannot be easily casted into a string) - $aPost[$sKey] = @(string) $sValue; + $aPost[$sKey] = @(string)$sValue; } } $this->Set('arguments_post', $aPost); - } - else - { + } else { $this->Set('arguments_post', array()); } - - $sLength = strlen($this->Get('issue')); - if ($sLength > 255) - { - $this->Set('issue', substr($this->Get('issue'), 0, 200)." -truncated ($sLength chars)"); + $sLength = mb_strlen($this->Get('issue')); + if ($sLength > 255) { + $this->Set('issue', mb_substr($this->Get('issue'), 0, 210)." -truncated ($sLength chars)"); } - $sLength = strlen($this->Get('impact')); - if ($sLength > 255) - { - $this->Set('impact', substr($this->Get('impact'), 0, 200)." -truncated ($sLength chars)"); + $sLength = mb_strlen($this->Get('impact')); + if ($sLength > 255) { + $this->Set('impact', mb_substr($this->Get('impact'), 0, 210)." -truncated ($sLength chars)"); } - $sLength = strlen($this->Get('page')); - if ($sLength > 255) - { - $this->Set('page', substr($this->Get('page'), 0, 200)." -truncated ($sLength chars)"); + $sLength = mb_strlen($this->Get('page')); + if ($sLength > 255) { + $this->Set('page', mb_substr($this->Get('page'), 0, 210)." -truncated ($sLength chars)"); } } } diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index 5f4d559f9..5c887e4b0 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -1930,17 +1930,13 @@ class SynchroLog extends DBObject $oAttDef = MetaModel::GetAttributeDef(get_class($this), 'traces'); $iMaxSize = $oAttDef->GetMaxSize(); - if (strlen($sPrevTrace) > 0) - { + if (strlen($sPrevTrace) > 0) { $sTrace = $sPrevTrace."\n".implode("\n", $this->m_aTraces); - } - else - { + } else { $sTrace = implode("\n", $this->m_aTraces); } - if (strlen($sTrace) >= $iMaxSize) - { - $sTrace = substr($sTrace, 0, $iMaxSize - 255)."...\nTruncated (size: ".strlen($sTrace).')'; + if (mb_strlen($sTrace) >= $iMaxSize) { + $sTrace = mb_substr($sTrace, 0, $iMaxSize - 40)."...\nTruncated (size: ".mb_strlen($sTrace).')'; } $this->Set('traces', $sTrace); @@ -2145,9 +2141,8 @@ class SynchroReplica extends DBObject implements iDisplay break; } - if (strlen($sWarningMessage) > $MAX_WARNING_LENGTH) - { - $sWarningMessage = substr($sWarningMessage, 0, $MAX_WARNING_LENGTH - 3).'...'; + if (mb_strlen($sWarningMessage) > $MAX_WARNING_LENGTH) { + $sWarningMessage = mb_substr($sWarningMessage, 0, $MAX_WARNING_LENGTH - 3).'...'; } $this->Set('status_last_warning', $sWarningMessage); @@ -2187,17 +2182,13 @@ class SynchroReplica extends DBObject implements iDisplay public function SetLastError($sMessage, $oException = null) { - if ($oException) - { + if ($oException) { $sText = $sMessage.$oException->getMessage(); - } - else - { + } else { $sText = $sMessage; } - if (strlen($sText) > 255) - { - $sText = substr($sText, 0, 200).'...('.strlen($sText).' chars)...'; + if (mb_strlen($sText) > 255) { + $sText = mb_substr($sText, 0, 200).'...('.mb_strlen($sText).' chars)...'; } $this->Set('status_last_error', $sText); } diff --git a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php index 5545c5cef..608297a5a 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php @@ -32,6 +32,7 @@ use Organization; use Person; use Team; use User; +use UserRequest; use UserRights; use utils; @@ -47,6 +48,7 @@ class DBObjectTest extends ItopDataTestCase // Counts public $aReloadCount = []; + protected function setUp(): void { parent::setUp(); @@ -1199,7 +1201,7 @@ class DBObjectTest extends ItopDataTestCase { return $this->aReloadCount[$sClass][$sKey] ?? 0; } - + /** * @since 3.1.0-3 3.1.1 3.2.0 N°6716 test creation */ @@ -1235,4 +1237,97 @@ class DBObjectTest extends ItopDataTestCase $fTotalDuration = microtime(true) - $fStart; echo 'Total duration: '.sprintf('%.3f s', $fTotalDuration)."\n\n"; } + + public function CheckLongValueInAttributeProvider() { + 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], + + // UserRequest.solution is an AttributeText (maxsize=65535) with format=text + 'solution 250 chars' => ['solution', 250], + 'solution 60000 chars' => ['solution', 60000], + 'solution 65534 chars' => ['solution', 65534], + 'solution 65535 chars' => ['solution', 65535], + 'solution 65536 chars' => ['solution', 65536], + 'solution 70000 chars' => ['solution', 70000], + ]; + } + + /** + * Test check long field with non ascii characters + * + * @covers DBObject::Set + * @covers DBObject::CheckToWrite + * @covers DBObject::SetTrim + * + * @dataProvider CheckLongValueInAttributeProvider + * + * @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) + { + $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 + $sEmojiRepeats = str_repeat($sEmojiToRepeat, $iValueLength - mb_strlen($sPrefix)); + $sValueToSet = $sPrefix . $sEmojiRepeats; + + $oTicket = MetaModel::NewObject('UserRequest', [ + 'ref' => 'Test Ticket', + 'title' => 'Create OK', + 'description' => 'Create OK', + 'caller_id' => 15, + 'org_id' => 3, + ]); + + $oTicket->Set($sAttrCode, $sValueToSet); + $sValueInObject = $oTicket->Get($sAttrCode); + $this->assertSame($sValueToSet, $sValueInObject, 'Set should not alter the value even if the value is too long'); + + $oAttDef = MetaModel::GetAttributeDef(UserRequest::class, $sAttrCode); + $iAttrMaxSize = $oAttDef->GetMaxSize(); + $bIsValueToSetBelowAttrMaxSize = ($iValueLength <= $iAttrMaxSize); + /** @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); + 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'); + } + } + + public function SetTrimProvider() + { + return [ + 'short string should not be truncated' => ['name','name'], + 'simple ascii string longer than 255 characters truncated' => [ + str_repeat('e',300), + str_repeat('e',232) . ' -truncated (300 chars)' + ], + 'smiley string longer than 255 characters truncated' => [ + str_repeat('😃',300), + str_repeat('😃',232) . ' -truncated (300 chars)' + ], + + ]; + } + + /** + * @dataProvider SetTrimProvider + * @return void + */ + public function testSetTrim($sName, $sResult){ + $oOrganisation = MetaModel::NewObject(Organization::class); + $oOrganisation->SetTrim('name', $sName); + $this->assertEquals($sResult, $oOrganisation->Get('name'), 'SetTrim must limit string to 255 characters'); + } } diff --git a/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php new file mode 100644 index 000000000..62faa35a4 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php @@ -0,0 +1,69 @@ + [ + str_repeat('é', 120), + str_repeat('é', 120), + str_repeat('é', 120), + str_repeat('é', 120), + ], + 'all with 255 smiley' => [ + str_repeat('😎', 255), + str_repeat('😎', 255), + str_repeat('😎', 255), + str_repeat('😎', 255), + ], + 'all with 255 characters, us-ascii only' => [ + str_repeat('a', 255), + str_repeat('a', 255), + str_repeat('a', 255), + str_repeat('a', 255), + ], + 'all with 255 é' => [ + str_repeat('é', 255), + str_repeat('é', 255), + str_repeat('é', 255), + str_repeat('é', 255), + ], + ]; + } + + /** + * EventIssue has a OnInsert override that uses mb_strlen, so we need to test this specific case + * + * @covers EventIssue::OnInsert + * + * @dataProvider OnInsertProvider + * + * @since 3.1.2 N°3448 - Framework field size check not correctly implemented for multi-bytes languages/strings + */ + public function testOnInsert(string $sIssue, string $sImpact, string $sPage, string $sMessage) + { + $oEventIssue = MetaModel::NewObject('EventIssue', [ + 'issue' => $sIssue, + 'impact' => $sImpact, + 'page' => $sPage, + 'message' => $sMessage, + ]); + + try { + $oEventIssue->DBInsert(); + } + catch (CoreException $e) { + $this->fail('we should be able to persist the object though it contains long values in its attributes'); + } + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/webservices/webservices.class.inc.php b/webservices/webservices.class.inc.php index f0bbc71e3..6a48646be 100644 --- a/webservices/webservices.class.inc.php +++ b/webservices/webservices.class.inc.php @@ -298,9 +298,8 @@ abstract class WebServicesBase if (is_object($oAttDef)) { $iMaxSize = $oAttDef->GetMaxSize(); - if ($iMaxSize && (strlen($sValue) > $iMaxSize)) - { - $sValue = substr($sValue, 0, $iMaxSize); + if ($iMaxSize && (mb_strlen($sValue) > $iMaxSize)) { + $sValue = mb_substr($sValue, 0, $iMaxSize); } $oLog->Set($sAttCode, $sValue); }