diff --git a/application/ui.htmleditorwidget.class.inc.php b/application/ui.htmleditorwidget.class.inc.php index 440124cc2e..3bc22b6163 100644 --- a/application/ui.htmleditorwidget.class.inc.php +++ b/application/ui.htmleditorwidget.class.inc.php @@ -72,15 +72,16 @@ public function Display(WebPage $oPage, $aArgs = array()) // To change the default settings of the editor, // a) edit the file /js/ckeditor/config.js // b) or override some of the configuration settings, using the second parameter of ckeditor() + $sJSDefineWidth = ''; $aConfig = utils::GetCkeditorPref(); $sWidthSpec = addslashes(trim($this->m_oAttDef->GetWidth())); - if ($sWidthSpec != '') - { - $aConfig['width'] = $sWidthSpec; + if ($sWidthSpec != '') { + /*N°6543 - the function min allow to keep text inside the column when width is defined*/ + $aConfig['width'] = "min($sWidthSpec,100%)"; + $sJSDefineWidth = '$("#cke_'.$iId.' iframe").contents().find("body").css("width", "'.$sWidthSpec.'")'; } $sHeightSpec = addslashes(trim($this->m_oAttDef->GetHeight())); - if ($sHeightSpec != '') - { + if ($sHeightSpec != '') { $aConfig['height'] = $sHeightSpec; } $sConfigJS = json_encode($aConfig); @@ -111,6 +112,7 @@ public function Display(WebPage $oPage, $aArgs = array()) else { oMe.data('ckeditorInstance').setReadOnly(oMe.prop('disabled')); + $sJSDefineWidth } }; setTimeout(delayedSetReadOnly, 50); diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 8416be1d6e..bc4b29336a 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -4535,7 +4535,6 @@ public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) $sStyle = ''; if (count($aStyles) > 0) { - $aStyles[] = 'overflow:auto'; $sStyle = 'style="'.implode(';', $aStyles).'"'; } diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index c9ccb436f9..ca0413b5b3 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -335,7 +335,7 @@ protected function RecordObjDeletion($objkey) $oMyChangeOp->Set("objclass", MetaModel::GetRootClass(get_class($this))); $oMyChangeOp->Set("objkey", $objkey); $oMyChangeOp->Set("fclass", get_class($this)); - $oMyChangeOp->Set("fname", substr($this->GetRawName(), 0, 255)); // Protect against very long friendly names + $oMyChangeOp->SetTrim("fname", $this->GetRawName()); // Protect against very long friendly names $iId = $oMyChangeOp->DBInsertNoReload(); } diff --git a/css/backoffice/components/_field.scss b/css/backoffice/components/_field.scss index 642715bd2a..6b20c1e518 100644 --- a/css/backoffice/components/_field.scss +++ b/css/backoffice/components/_field.scss @@ -80,16 +80,29 @@ $ibo-field--enable-bulk--checkbox--margin-left: $ibo-spacing-300 !default; } } } + + /*N°6543 - We need the rule to keep text inside the column when width is defined*/ + &[data-attribute-type="AttributeHtml"], + &[data-attribute-type="AttributeText"] { + &[data-attribute-flag-read-only="true"] { + display: grid; + + > .ibo-field--value { + max-width: 100%; + overflow: auto; + } + } + } } -/* Large field = Label on top, value below */ -.ibo-field-large { - display: block; + /* Large field = Label on top, value below */ + .ibo-field-large { + display: block; - .ibo-field--label { - position: relative; /* Necessary for fullscreen toggler */ - display: flex; - align-items: center; + .ibo-field--label { + position: relative; /* Necessary for fullscreen toggler */ + display: flex; + align-items: center; max-width: initial; width: 100%; } diff --git a/css/backoffice/components/datatable/_datatable.scss b/css/backoffice/components/datatable/_datatable.scss index 95797d908a..c8147baa67 100644 --- a/css/backoffice/components/datatable/_datatable.scss +++ b/css/backoffice/components/datatable/_datatable.scss @@ -123,6 +123,13 @@ $ibo-fieldsorter--selected--background-color: $ibo-color-blue-200 !default; .ibo-datatable--row-actions-toolbar{ justify-content: end; } + /* N°6543 - We need the rule to keep text inside the column when width is defined */ + > [data-attribute-type="AttributeHtml"], + > [data-attribute-type="AttributeText"] { + max-width: 100%; + overflow: auto; + } + } } diff --git a/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml b/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml index 50271092f6..6e0acee097 100755 --- a/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml +++ b/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml @@ -269,16 +269,15 @@ An attachment has been added to an object Attachment::AfterUpdate - Attachment cmdbAbstractObject - The attachment updated + The object where the attachment is added DBObject - - The object to which the attachment is linked + + The attachment added to the objet DBObject @@ -291,16 +290,15 @@ An attachment has been removed from an object Attachment::AfterUpdate - Attachment cmdbAbstractObject - The attachment updated + The object where the attachment is removed DBObject - - The object to which the attachment is linked + + The attachment removed DBObject diff --git a/datamodels/2.x/itop-attachments/main.itop-attachments.php b/datamodels/2.x/itop-attachments/main.itop-attachments.php index 0f70f34b89..94a1e72699 100644 --- a/datamodels/2.x/itop-attachments/main.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/main.itop-attachments.php @@ -264,7 +264,6 @@ public function DisplayAttachments(DBObject $oObject, WebPage $oPage, $bEditMode else { $oAttachmentsRenderer->RenderViewAttachmentsList(); - } } @@ -293,8 +292,8 @@ protected static function UpdateAttachments($oObject, $oChange = null) // Remove attachments that are no longer attached to the current object if (in_array($oAttachment->GetKey(), $aRemovedAttachmentIds)) { - $aData = ['target_object' => $oObject]; - $oAttachment->FireEvent(EVENT_REMOVE_ATTACHMENT_FROM_OBJECT, $aData); + $aData = ['attachment' => $oAttachment]; + $oObject->FireEvent(EVENT_REMOVE_ATTACHMENT_FROM_OBJECT, $aData); $oAttachment->DBDelete(); $aActions[] = self::GetActionChangeOp($oAttachment, false /* false => deletion */); } @@ -322,8 +321,8 @@ protected static function UpdateAttachments($oObject, $oChange = null) $oAttachment->DBUpdate(); // temporary attachment confirmed, list it in the history $aActions[] = self::GetActionChangeOp($oAttachment, true /* true => creation */); - $aData = ['target_object' => $oObject]; - $oAttachment->FireEvent(EVENT_ADD_ATTACHMENT_TO_OBJECT, $aData); + $aData = ['attachment' => $oAttachment]; + $oObject->FireEvent(EVENT_ADD_ATTACHMENT_TO_OBJECT, $aData); } } if (count($aActions) > 0) diff --git a/templates/base/components/datatable/row-actions/handler.js.twig b/templates/base/components/datatable/row-actions/handler.js.twig index 38c699f8c0..56c0811f96 100644 --- a/templates/base/components/datatable/row-actions/handler.js.twig +++ b/templates/base/components/datatable/row-actions/handler.js.twig @@ -23,16 +23,16 @@ {% if aAction.confirmation is defined %} // Prepare confirmation title - let sTitle = '{{ 'UI:Datatables:RowActions:ConfirmationDialog'|dict_s }}'; + let sTitle = `{{ 'UI:Datatables:RowActions:ConfirmationDialog'|dict_s|escape('js') }}`; {% if aAction.confirmation.title is defined %} - sTitle = '{{ aAction.confirmation.title|dict_s }}'; + sTitle = `{{ aAction.confirmation.title|dict_s|escape('js') }}`; {% endif %} sTitle = sTitle.replaceAll('{item}', aRowData['{{ aAction.confirmation.row_data }}']); // Prepare confirmation message - let sMessage = '{{ 'UI:Datatables:RowActions:ConfirmationMessage'|dict_s }}'; + let sMessage = `{{ 'UI:Datatables:RowActions:ConfirmationMessage'|dict_s|escape('js') }}`; {% if aAction.confirmation.message is defined %} - sMessage = '{{ aAction.confirmation.message|dict_s }}'; + sMessage = `{{ aAction.confirmation.message|dict_s|escape('js') }}`; {% endif %} sMessage = sMessage.replaceAll('{item}', aRowData['{{ aAction.confirmation.row_data }}']); diff --git a/tests/php-unit-tests/unitary-tests/core/CMDBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/CMDBObjectTest.php index 4d2e2ad9fc..8188ec08f6 100644 --- a/tests/php-unit-tests/unitary-tests/core/CMDBObjectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/CMDBObjectTest.php @@ -5,9 +5,11 @@ use CMDBObject; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; +use CoreException; use Exception; use MetaModel; + /** * @since 2.7.7 3.0.2 3.1.0 N°3717 tests history objects creation * @@ -168,6 +170,53 @@ public function testCurrentChangeUnderImpersonation($sTrackInfo=null, $sExpected CMDBObject::SetTrackInfo($sInitialTrackInfo); } + /** + * Data provider for test deletion + * N°5547 - Object deletion fails if friendlyname too long + * + * @return array data + */ + public function RecordObjDeletionProvider() + { + return [ + 'friendlyname longer than 255 characters which will be truncated on a multi-bytes characters' => [ + str_repeat('e', 250), + '😁😂🤣😃😄😅😆😗🥰😘😍😎😋😊😉😙😚', + ], + 'friendlyname longer than 255 characters which will be truncated after a single byte characters' => [ + '😁😂🤣😃😄😅😆😗🥰😘😍😎😋😊😉😙😚', + str_repeat('e', 250), + ], + ]; + } + + /** + * N°5547 - Object deletion fails if friendlyname too long + * + * @dataProvider RecordObjDeletionProvider + * + */ + public function testRecordObjDeletion( string $sFirstName, string $sName) + { + $oPerson = MetaModel::NewObject('Person', [ + 'first_name' => $sFirstName, + 'name' => $sName, + 'org_id' => 1, + ]); + $oPerson->DBWrite(); + + $bDeletionOK = true; + try { + $oDeletionPlan = $this->InvokeNonPublicMethod(CMDBObject::class, 'RecordObjDeletion', $oPerson, [$oPerson->GetKey()]); + } + catch (CoreException $e) { + $bDeletionOK = false; + } + // We don't need to test the result (truncated string), it's already done in \DBObject::SetTrim() with N°3448 + $this->assertTrue($bDeletionOK); + } + + private function ReplaceByFriendlyNames($sMessage, $oAdminUser, $oImpersonatedUser) : string { $sNewMessage = str_replace('AdminSurName AdminName', $oAdminUser->GetFriendlyName(), $sMessage); $sNewMessage = str_replace('ImpersonatedSurName ImpersonatedName', $oImpersonatedUser->GetFriendlyName(), $sNewMessage); diff --git a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php index 433032aca3..321610d78b 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php @@ -1133,6 +1133,54 @@ private function CreatePersonInstance() return $oPerson; } + /** + * Data provider for test deletion + * N°5547 - Object deletion fails if friendlyname too long + * + * @return array data + */ + public function getDeletionLongValueProvider() + { + return [ + 'friendlyname longer than 255 chracters with smiley' => [ + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789-0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq', + '😁😂🤣😃😄😅😆😗🥰😘😍😎😋😊😉😙😚', + ], + 'the same friendlyname in other order with error before fix 5547 ' => [ + '😁😂🤣😃😄😅😆😗🥰😘😍😎😋😊😉😙😚', + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789-0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq', + ], + ]; + } + + /** + * N°5547 - Object deletion fails if friendlyname too long + * + * @covers DBObject::DBIncrement + * + * @dataProvider getDeletionLongValueProvider + * + */ + public function testDeletionLongValue(string $sName, string $sFirstName) + { + // Create a UserRequest with 2 contacts + $oPerson = MetaModel::NewObject('Person', [ + 'name' => $sName, + 'first_name' => $sFirstName, + 'org_id' => 1, + ]); + $oPerson->DBWrite(); + + $bDeletionOK = true; + try { + $oDeletionPlan = $oPerson->DBDelete(); + } + catch (CoreException $e) { + $bDeletionOK = false; + } + $this->assertTrue($bDeletionOK); + } + public function ResetReloadCount() { $this->aReloadCount = []; @@ -1211,8 +1259,14 @@ public function testConstructorMemoryFootprint():void $fTotalDuration = microtime(true) - $fStart; echo 'Total duration: '.sprintf('%.3f s', $fTotalDuration)."\n\n"; } - - public function CheckLongValueInAttributeProvider() { + /** + * Data provider for test deletion + * N°5547 - Object deletion fails if friendlyname too long + * + * @return array data + */ + public function DeletionLongValueProvider() + { return [ // UserRequest.title is an AttributeString (maxsize = 255) 'title 250 chars' => ['title', 250], @@ -1234,11 +1288,9 @@ public function CheckLongValueInAttributeProvider() { /** * Test check long field with non ascii characters * - * @covers DBObject::Set - * @covers DBObject::CheckToWrite - * @covers DBObject::SetTrim + * @covers DBObject::DBDelete * - * @dataProvider CheckLongValueInAttributeProvider + * @dataProvider DeletionLongValueProvider * * @since 3.1.2 N°3448 - Framework field size check not correctly implemented for multi-bytes languages/strings */ diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-attachments/TestAttachment.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-attachments/TestAttachment.php new file mode 100644 index 0000000000..8485c956f0 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-attachments/TestAttachment.php @@ -0,0 +1,72 @@ +sAddAttachmentName = ''; + $this->sRemoveAttachmentName = ''; + + $_REQUEST['transaction_id'] = 'test_transaction'; + $_REQUEST['attachment_plugin'] = 'in_form'; + + $oDocument = new \ormDocument('Test', 'text/plain', 'test.txt'); + + $this->EventService_RegisterListener(EVENT_ADD_ATTACHMENT_TO_OBJECT, [$this, 'OnAddAttachment']); + $this->EventService_RegisterListener(EVENT_REMOVE_ATTACHMENT_FROM_OBJECT, [$this, 'OnRemoveAttachment']); + + $oAttachment = MetaModel::NewObject('Attachment', [ + 'item_class' => 'UserRequest', + 'temp_id' => 'test_transaction', + 'contents' => $oDocument, + ]); + + $oAttachment->DBInsert(); + $oTicket = $this->CreateTicket(1); + + $_REQUEST['removed_attachments'] = [$oAttachment->GetKey()]; + $this->InvokeNonPublicStaticMethod(\AttachmentPlugIn::class, 'UpdateAttachments', [$oTicket]); + + $this->assertEquals('test.txt', $this->sAddAttachmentName); + $this->assertEquals('test.txt', $this->sRemoveAttachmentName); + } + + public function OnAddAttachment(EventData $oData) + { + $this->debug('OnAddAttachment'); + $this->assertEquals('UserRequest', get_class($oData->Get('object'))); + $oAttachment = $oData->Get('attachment'); + /** @var \ormDocument $oDocument */ + $oDocument = $oAttachment->Get('contents'); + $this->sAddAttachmentName = $oDocument->GetFileName(); + } + + public function OnRemoveAttachment(EventData $oData) + { + $this->debug('OnRemoveAttachment'); + $this->assertEquals('UserRequest', get_class($oData->Get('object'))); + $oAttachment = $oData->Get('attachment'); + /** @var \ormDocument $oDocument */ + $oDocument = $oAttachment->Get('contents'); + $this->sRemoveAttachmentName = $oDocument->GetFileName(); + } +}