From 7cac280b830bece4352515ccb056586553e69110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Harnes?= Date: Mon, 4 May 2026 16:33:38 +0200 Subject: [PATCH 01/10] =?UTF-8?q?N=C2=B09574=20-=20Fix=20CKEditor=20CSS=20?= =?UTF-8?q?displayed=20as=20part=20of=20the=20email=20message=20in=20Gmail?= =?UTF-8?q?=20(#898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(email): generate plain text before inlining HTML CSS * N°9574 - Add unit tests * Apply suggestions from code review Co-authored-by: Molkobain --------- Co-authored-by: Molkobain --- sources/Core/Email/EmailSymfony.php | 7 +- .../sources/core/Email/EmailSymfonyTest.php | 163 ++++++++++++++++++ 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/sources/Core/Email/EmailSymfony.php b/sources/Core/Email/EmailSymfony.php index e397345c4d..1d29aec200 100644 --- a/sources/Core/Email/EmailSymfony.php +++ b/sources/Core/Email/EmailSymfony.php @@ -30,6 +30,7 @@ use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mime\Email as SymfonyEmail; +use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\Multipart\RelatedPart; use Symfony\Component\Mime\Part\Multipart\MixedPart; @@ -416,13 +417,13 @@ class EMailSymfony extends Email $this->m_aData['body'] = ['body' => $sBody, 'mimeType' => $sMimeType]; - $oTextPart = new TextPart(strip_tags($sBody), 'utf-8', 'plain', 'base64'); - // Embed inline images and store them in attachments (so BuildSymfonyMessageFromInternal can pick them) if ($sPrimaryMimeType === 'text/html') { $aAdditionalParts = $this->EmbedInlineImages($sBody); + $oTextPart = new TextPart((new DefaultHtmlToTextConverter())->convert($sBody, 'utf-8'), 'utf-8', 'plain', 'base64'); $oHtmlPart = new TextPart($sBody, 'utf-8', 'html', 'base64'); - $oAlternativePart = new AlternativePart($oHtmlPart, $oTextPart); + // It's important de order parts from least prefered to most prefered as per RFC 2046 {@see https://www.rfc-editor.org/rfc/rfc2046.html#section-5.1.4} + $oAlternativePart = new AlternativePart($oTextPart, $oHtmlPart); // Default root part is the HTML body $oRootPart = $oAlternativePart; diff --git a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php index 40594eccbf..1a289798d5 100644 --- a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php @@ -1,6 +1,11 @@ assertSame($sExpectedBody, $sActualBody); } + + /** + * Returns the parts of the AlternativePart produced by SetBody() for an HTML email. + * + * Handles both the simple case (AlternativePart at root) and the inline-images case + * where the root is a RelatedPart whose first child is the AlternativePart. + * + * @return AbstractPart[] + */ + private function GetAlternativePartsFromHtmlEmail(EMailSymfony $oEmail): array + { + $oSymfonyMessage = $this->GetNonPublicProperty($oEmail, 'm_oMessage'); + $oBody = $oSymfonyMessage->getBody(); + + // With inline images the root is a RelatedPart; the AlternativePart is its first child. + if ($oBody instanceof RelatedPart) { + $oBody = $oBody->getParts()[0]; + } + + $this->assertInstanceOf(AlternativePart::class, $oBody, 'Body should be a multipart/alternative for HTML emails'); + + return $oBody->getParts(); + } + + /** + * RFC 2046 §5.1.4: parts in multipart/alternative must be ordered from least to most preferred. + * Email clients display the last part they support, so text/plain must come first and text/html last. + * + * @see https://www.rfc-editor.org/rfc/rfc2046.html#section-5.1.4 + * @covers \Combodo\iTop\Core\Email\EmailSymfony::SetBody() + * @since N°9574 + */ + public function testSetBodyAlternativePartOrderForHtmlEmailIsPlainThenHtml(): void + { + $oEmail = new EMailSymfony(); + $oEmail->SetBody('

Hello there!

', 'text/html'); + + [$oFirstPart, $oSecondPart] = $this->GetAlternativePartsFromHtmlEmail($oEmail); + + $this->assertSame('plain', $oFirstPart->getMediaSubtype(), 'First part must be text/plain (least preferred per RFC 2046)'); + $this->assertSame('html', $oSecondPart->getMediaSubtype(), 'Last part must be text/html (most preferred per RFC 2046)'); + } + + /** + * @dataProvider provideSetBodyPlainTextDoesNotContainCss + * + * @covers \Combodo\iTop\Core\Email\EmailSymfony::SetBody() + * @since N°9574 + */ + public function testSetBodyPlainTextDoesNotContainCss(string $sHtml, ?string $sCustomStyles): void + { + $oEmail = new EMailSymfony(); + $oEmail->SetBody($sHtml, 'text/html', $sCustomStyles); + + // We locate the plain text part by subtype to be order-agnostic and isolate this assertion from the order bug. + $aParts = $this->GetAlternativePartsFromHtmlEmail($oEmail); + $oPlainPart = null; + foreach ($aParts as $oPart) { + if ($oPart instanceof TextPart && $oPart->getMediaSubtype() === 'plain') { + $oPlainPart = $oPart; + break; + } + } + $this->assertNotNull($oPlainPart, 'No text/plain part found in the message'); + + $sPlainText = $oPlainPart->getBody(); + + $this->assertStringNotContainsString('

Hello there!

', 'text/html'); + + $oSymfonyMessage = $this->GetNonPublicProperty($oEmail, 'm_oMessage'); + $oBody = $oSymfonyMessage->getBody(); + + // Root must be a RelatedPart when inline images are present + $this->assertInstanceOf(RelatedPart::class, $oBody, 'Root part must be multipart/related when inline images are present'); + + // The AlternativePart must be the first child of the RelatedPart + $aRelatedParts = $oBody->getParts(); + $this->assertInstanceOf(AlternativePart::class, $aRelatedParts[0], 'First child of RelatedPart must be the AlternativePart'); + + // Order and CSS checks are delegated to the shared helper, which now handles RelatedPart + [$oFirstPart, $oSecondPart] = $this->GetAlternativePartsFromHtmlEmail($oEmail); + $this->assertSame('plain', $oFirstPart->getMediaSubtype(), 'First part must be text/plain (least preferred per RFC 2046)'); + $this->assertSame('html', $oSecondPart->getMediaSubtype(), 'Last part must be text/html (most preferred per RFC 2046)'); + } + + public function provideSetBodyPlainTextDoesNotContainCss(): array + { + $sCustomStyles = 'p { color: blue; font-size: 14px; }'; + + return [ + '

Hello there!

', + null, + ], + '

Hello there!

', + $sCustomStyles, + ], + 'custom styles only, no + + + + + + + + + + + + + + + + status='inactive' + + + status + + + + + name + + false + + + org_id + + + false + Organization + DEL_MANUAL + all + + + source_id + + + false + FunctionalCI + DEL_MANUAL + all + + + rank + + + yes + 10 + + + no + 20 + + + source_impact + yes + false + radio_horizontal + + + destination_id + + + false + FunctionalCI + DEL_MANUAL + all + + + rank + + + yes + 10 + + + no + 20 + + + destination_impact + no + false + radio_horizontal + + + dataflowtype_id + + + true + DataFlowType + DEL_MANUAL + all + + + description + + true + all + + + status + + + active + 10 + + + + inactive + 20 + + + + label + active + false + list + all + + + rank + + + high + 10 + + + medium + 20 + + + low + 30 + + + business_criticity + low + false + list + + + rank + + + realtime + 10 + + + ondemand + 20 + + + hourly + 30 + + + daily + 40 + + + weekly + 50 + + + monthly + 60 + + + yearly + 70 + + + execution_frequency + daily + false + list + + + lnkContactToDataFlow + dataflow_id + 0 + 0 + contact_id + + + + lnkDocumentToDataFlow + dataflow_id + 0 + 0 + document_id + + + + + + + + + 10 + + + 20 + + + 30 + + + 40 + + + 50 + + + + + + + 10 + + + 20 + + + 30 + + + 40 + + + +
+ + + + + + + 10 + + + 20 + + + 30 + + + 40 + + + 10 + + + + + 10 + + + 20 + + + 30 + + + 40 + + + 50 + + + 60 + + + 20 + + + 10 + + + + + + + 10 + + + 10 + + + 20 + + + 70 + + + 80 + + +
+ + + + 10 + + + 20 + + + 30 + + + 40 + + + 50 + + + + + + + 10 + + + 20 + + + +
+ + + + + destination_impact = 'yes' AND id = :this->destination_id]]> + id]]> + both + + + contacts_list + down + + + + + + + cmdbAbstractObject + + 1 + bizmodel + false + autoincrement + lnkdocumenttodataflow + id + + + + + + + + + + + + + + + + + + + + + + false + true + + + + + + dataflow_id + DataFlow + false + DEL_AUTO + + + document_id + Document + false + DEL_AUTO + + + + +
+ + + 10 + + + 20 + + +
+ + + + 10 + + + 20 + + + + + + + 10 + + + 20 + + + +
+
+ + cmdbAbstractObject + + 1 + bizmodel + false + autoincrement + lnkcontacttodataflow + id + + + + + + + + + + + + + + + + + + + + + + false + true + + + + + + dataflow_id + DataFlow + false + DEL_AUTO + + + contact_id + Contact + false + DEL_AUTO + + + + +
+ + + 10 + + + 20 + + +
+ + + + 10 + + + 20 + + + + + + + 10 + + + 20 + + + +
+
+ + Typology + + bizmodel,searchable + false + dataflowtype + + + + + + + + + + + + + + + + + + + 10 + + + + + + + 10 + + + +
+ + + 10 + + +
+
+
+ + + + true + + DashboardLayoutTwoCols + FunctionalCI:DataFlow:Title + + false + 300 + + + + 0 + + + 0 + FunctionalCI:DataFlow:Inbound + SELECT DataFlow WHERE destination_id=:this->id + true + + + + + 1 + + + 0 + FunctionalCI:DataFlow:Outbound + SELECT DataFlow WHERE source_id=:this->id + true + + + + + + + + + + + + id AND source_impact = 'yes']]> + id]]> + both + + + + + + + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + +
+ + + 25 + + +
+
+
+ + + + + + + + + 20 + DataFlow + + + + + + + + + + + + + + + + + + + diff --git a/datamodels/2.x/itop-flow-map/dictionaries/en.dict.itop-flow-map.php b/datamodels/2.x/itop-flow-map/dictionaries/en.dict.itop-flow-map.php new file mode 100644 index 0000000000..415129609b --- /dev/null +++ b/datamodels/2.x/itop-flow-map/dictionaries/en.dict.itop-flow-map.php @@ -0,0 +1,96 @@ + 'Data flows', + 'Class:FunctionalCI/Attribute:dataflows+' => 'Data flows for which this object is the source or the destination', + 'FunctionalCI:DataFlow:Title' => 'Data flows', + 'FunctionalCI:DataFlow:Inbound' => 'Inbound flows', + 'FunctionalCI:DataFlow:Outbound' => 'Outbound flows', + + 'DataFlow:baseinfo' => 'General information', + 'DataFlow:otherinfo' => 'Other information', + 'DataFlow:moreinfo' => 'Flow specifics', + + 'Class:DataFlow' => 'Flow', + 'Class:DataFlow+' => 'For application flow for example', + 'Class:DataFlow/Name' => '%1$s', + 'Class:DataFlow/Attribute:name' => 'Name', + 'Class:DataFlow/Attribute:name_id+' => 'Data that are transferred', + 'Class:DataFlow/Attribute:source_id' => 'Source', + 'Class:DataFlow/Attribute:source_id+' => 'Source Ci of the flow', + 'Class:DataFlow/Attribute:source_impact' => 'Source impacts?', + 'Class:DataFlow/Attribute:source_impact+' => 'Does the source impact the flow?', + 'Class:DataFlow/Attribute:source_impact/Value:yes' => 'yes', + 'Class:DataFlow/Attribute:source_impact/Value:yes+' => 'If the source falls down, the flow is impacted', + 'Class:DataFlow/Attribute:source_impact/Value:no' => 'no', + 'Class:DataFlow/Attribute:source_impact/Value:no+' => 'If the source falls down, the flow is not impacted', + 'Class:DataFlow/Attribute:destination_id' => 'Destination', + 'Class:DataFlow/Attribute:destination_id+' => 'Destination Ci for the flow', + 'Class:DataFlow/Attribute:destination_impact' => 'Destination impacted', + 'Class:DataFlow/Attribute:destination_impact+' => 'Is the destination impacted by the flow ?', + 'Class:DataFlow/Attribute:destination_impact/Value:yes' => 'yes', + 'Class:DataFlow/Attribute:destination_impact/Value:yes+' => 'If the flow stops, the destination is impacted', + 'Class:DataFlow/Attribute:destination_impact/Value:no' => 'no', + 'Class:DataFlow/Attribute:destination_impact/Value:no+' => 'If the flow stops, the destination is not impacted', + 'Class:DataFlow/Attribute:dataflowtype_id' => 'Flow type', + 'Class:DataFlow/Attribute:dataflowtype_id+' => 'Typology of Flow.', + 'Class:DataFlow/Attribute:description' => 'Description', + 'Class:DataFlow/Attribute:description+' => '', + 'Class:DataFlow/Attribute:status' => 'Status', + 'Class:DataFlow/Attribute:status+' => '', + 'Class:DataFlow/Attribute:status/Value:active' => 'active', + 'Class:DataFlow/Attribute:status/Value:inactive' => 'inactive', + 'Class:DataFlow/Attribute:org_id' => 'Organization', + 'Class:DataFlow/Attribute:org_id+' => '', + 'Class:DataFlow/Attribute:business_criticity' => 'Business criticality', + 'Class:DataFlow/Attribute:business_criticity+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:high' => 'high', + 'Class:DataFlow/Attribute:business_criticity/Value:high+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:low' => 'low', + 'Class:DataFlow/Attribute:business_criticity/Value:low+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:medium' => 'medium', + 'Class:DataFlow/Attribute:business_criticity/Value:medium+' => '', + 'Class:DataFlow/Attribute:execution_frequency' => 'Execution frequency', + 'Class:DataFlow/Attribute:execution_frequency+' => 'How often the data flow is executed', + 'Class:DataFlow/Attribute:execution_frequency/Value:realtime' => 'real-time', + 'Class:DataFlow/Attribute:execution_frequency/Value:realtime+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:ondemand' => 'on demand', + 'Class:DataFlow/Attribute:execution_frequency/Value:ondemand+' => 'on the fly, not scheduled', + 'Class:DataFlow/Attribute:execution_frequency/Value:hourly' => 'hourly', + 'Class:DataFlow/Attribute:execution_frequency/Value:hourly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:daily' => 'daily', + 'Class:DataFlow/Attribute:execution_frequency/Value:daily+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:weekly' => 'weekly', + 'Class:DataFlow/Attribute:execution_frequency/Value:weekly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:monthly' => 'monthly', + 'Class:DataFlow/Attribute:execution_frequency/Value:monthly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:yearly' => 'yearly', + 'Class:DataFlow/Attribute:execution_frequency/Value:yearly+' => '', + 'Class:DataFlow/Attribute:documents_list' => 'Documents', + 'Class:DataFlow/Attribute:documents_list+' => 'Eg: technical specifications, runbooks, etc.', + 'Class:DataFlow/Attribute:contacts_list' => 'Contacts', + 'Class:DataFlow/Attribute:contacts_list+' => 'Eg: flow owner, technical support, etc.', + +/* + 'Class:DataFlow/Attribute:source_id_friendlyname' => 'source_id_friendlyname', + 'Class:DataFlow/Attribute:source_id_friendlyname+' => 'Full name', + 'Class:DataFlow/Attribute:source_id_finalclass_recall' => 'source_id->CI sub-class', + 'Class:DataFlow/Attribute:source_id_finalclass_recall+' => 'Name of the final class', + 'Class:DataFlow/Attribute:source_id_obsolescence_flag' => 'source_id->Obsolete', + 'Class:DataFlow/Attribute:source_id_obsolescence_flag+' => 'Computed dynamically on other attributes', + 'Class:DataFlow/Attribute:destination_id_friendlyname' => 'destination_id_friendlyname', + 'Class:DataFlow/Attribute:destination_id_friendlyname+' => 'Full name', + 'Class:DataFlow/Attribute:destination_id_finalclass_recall' => 'destination_id->CI sub-class', + 'Class:DataFlow/Attribute:destination_id_finalclass_recall+' => 'Name of the final class', + 'Class:DataFlow/Attribute:destination_id_obsolescence_flag' => 'destination_id->Obsolete', + 'Class:DataFlow/Attribute:destination_id_obsolescence_flag+' => 'Computed dynamically on other attributes', +*/ +]); diff --git a/datamodels/2.x/itop-flow-map/dictionaries/fr.dict.itop-flow-map.php b/datamodels/2.x/itop-flow-map/dictionaries/fr.dict.itop-flow-map.php new file mode 100644 index 0000000000..ea4e4afcaf --- /dev/null +++ b/datamodels/2.x/itop-flow-map/dictionaries/fr.dict.itop-flow-map.php @@ -0,0 +1,96 @@ + 'Flux de données', + 'Class:FunctionalCI/Attribute:dataflows+' => 'Flux de données dont cet objet est la source ou la destination', + 'FunctionalCI:DataFlow:Title' => 'Flux de données', + 'FunctionalCI:DataFlow:Inbound' => 'Flux entrants', + 'FunctionalCI:DataFlow:Outbound' => 'Flux sortants', + + 'DataFlow:baseinfo' => 'Informations générales', + 'DataFlow:otherinfo' => 'Autres informations', + 'DataFlow:moreinfo' => 'Spécificités du flux', + + 'Class:DataFlow' => 'Flux de Données', + 'Class:DataFlow+' => 'Modélise les données transférées entre instances d\'application', + 'Class:DataFlow/Name' => '%1$s', + 'Class:DataFlow/Attribute:name' => 'Nom', + 'Class:DataFlow/Attribute:name_id+' => 'Type de données transferées', + 'Class:DataFlow/Attribute:source_id' => 'Source', + 'Class:DataFlow/Attribute:source_id+' => 'Instance d\application à la source du flux de données', + 'Class:DataFlow/Attribute:source_impact' => 'Source impactante ?', + 'Class:DataFlow/Attribute:source_impact+' => 'La source impacte-t-elle le flux de données ?', + 'Class:DataFlow/Attribute:source_impact/Value:yes' => 'oui', + 'Class:DataFlow/Attribute:source_impact/Value:yes+' => 'Si la source tombe en panne, le flux de données est impacté', + 'Class:DataFlow/Attribute:source_impact/Value:no' => 'non', + 'Class:DataFlow/Attribute:source_impact/Value:no+' => 'Si la source tombe en panne, le flux de données n\'est pas impacté', + 'Class:DataFlow/Attribute:destination_id' => 'Destinataire', + 'Class:DataFlow/Attribute:destination_id+' => 'Destinataire des données, à choisir parmi les instances d\'application', + 'Class:DataFlow/Attribute:destination_impact' => 'Destinataire impacté ?', + 'Class:DataFlow/Attribute:destination_impact+' => 'Le destinataire est-il impacté si le flux de données s\'arrête ?', + 'Class:DataFlow/Attribute:destination_impact/Value:yes' => 'oui', + 'Class:DataFlow/Attribute:destination_impact/Value:yes+' => 'Si le flux s\'arrête, le destinataire est impacté', + 'Class:DataFlow/Attribute:destination_impact/Value:no' => 'non', + 'Class:DataFlow/Attribute:destination_impact/Value:no+' => 'Si le flux s\'arrête, le destinataire n\'est pas impacté', + 'Class:DataFlow/Attribute:dataflowtype_id' => 'Type de flux', + 'Class:DataFlow/Attribute:dataflowtype_id+' => 'Typologie du flux', + 'Class:DataFlow/Attribute:description' => 'Description', + 'Class:DataFlow/Attribute:description+' => '', + 'Class:DataFlow/Attribute:status' => 'Etat', + 'Class:DataFlow/Attribute:status+' => '', + 'Class:DataFlow/Attribute:status/Value:active' => 'actif', + 'Class:DataFlow/Attribute:status/Value:inactive' => 'inactif', + 'Class:DataFlow/Attribute:org_id' => 'Organisation', + 'Class:DataFlow/Attribute:org_id+' => '', + 'Class:DataFlow/Attribute:business_criticity' => 'Criticité', + 'Class:DataFlow/Attribute:business_criticity+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:high' => 'haute', + 'Class:DataFlow/Attribute:business_criticity/Value:high+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:low' => 'basse', + 'Class:DataFlow/Attribute:business_criticity/Value:low+' => '', + 'Class:DataFlow/Attribute:business_criticity/Value:medium' => 'moyenne', + 'Class:DataFlow/Attribute:business_criticity/Value:medium+' => '', + 'Class:DataFlow/Attribute:execution_frequency' => 'Fréquence d\'exécution', + 'Class:DataFlow/Attribute:execution_frequency+' => 'À quelle fréquence le transfert de données est-il exécuté', + 'Class:DataFlow/Attribute:execution_frequency/Value:realtime' => 'temps réel', + 'Class:DataFlow/Attribute:execution_frequency/Value:realtime+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:ondemand' => 'à la demande', + 'Class:DataFlow/Attribute:execution_frequency/Value:ondemand+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:hourly' => 'horaire', + 'Class:DataFlow/Attribute:execution_frequency/Value:hourly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:daily' => 'journalière', + 'Class:DataFlow/Attribute:execution_frequency/Value:daily+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:weekly' => 'hebdomadaire', + 'Class:DataFlow/Attribute:execution_frequency/Value:weekly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:monthly' => 'mensuelle', + 'Class:DataFlow/Attribute:execution_frequency/Value:monthly+' => '', + 'Class:DataFlow/Attribute:execution_frequency/Value:yearly' => 'annuelle', + 'Class:DataFlow/Attribute:execution_frequency/Value:yearly+' => '', + 'Class:DataFlow/Attribute:documents_list' => 'Documents', + 'Class:DataFlow/Attribute:documents_list+' => 'Eg: technical specifications, runbooks, etc.', + 'Class:DataFlow/Attribute:contacts_list' => 'Contacts', + 'Class:DataFlow/Attribute:contacts_list+' => 'Eg: flow owner, technical support, etc.', + +/* + 'Class:DataFlow/Attribute:source_id_friendlyname' => 'source_id_friendlyname', + 'Class:DataFlow/Attribute:source_id_friendlyname+' => 'Nom complet', + 'Class:DataFlow/Attribute:source_id_finalclass_recall' => 'source_id->CI sub-class', + 'Class:DataFlow/Attribute:source_id_finalclass_recall+' => 'Classe finale', + 'Class:DataFlow/Attribute:source_id_obsolescence_flag' => 'source_id->Obsolete', + 'Class:DataFlow/Attribute:source_id_obsolescence_flag+' => 'Computed dynamically on other attributes', + 'Class:DataFlow/Attribute:destination_id_friendlyname' => 'destination_id_friendlyname', + 'Class:DataFlow/Attribute:destination_id_friendlyname+' => 'Nom complet', + 'Class:DataFlow/Attribute:destination_id_finalclass_recall' => 'destination_id->CI sub-class', + 'Class:DataFlow/Attribute:destination_id_finalclass_recall+' => 'Classe finale', + 'Class:DataFlow/Attribute:destination_id_obsolescence_flag' => 'destination_id->Obsolete', + 'Class:DataFlow/Attribute:destination_id_obsolescence_flag+' => 'Computed dynamically on other attributes', +*/ +]); diff --git a/datamodels/2.x/itop-flow-map/images/icons8-sorting-arrows-horizontal.svg b/datamodels/2.x/itop-flow-map/images/icons8-sorting-arrows-horizontal.svg new file mode 100644 index 0000000000..6cee6f7a06 --- /dev/null +++ b/datamodels/2.x/itop-flow-map/images/icons8-sorting-arrows-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/datamodels/2.x/itop-flow-map/module.itop-flow-map.php b/datamodels/2.x/itop-flow-map/module.itop-flow-map.php new file mode 100644 index 0000000000..a89b41d2fb --- /dev/null +++ b/datamodels/2.x/itop-flow-map/module.itop-flow-map.php @@ -0,0 +1,50 @@ + 'Map applications data flows', + 'category' => 'business', + + // Setup + // + 'dependencies' => [ + 'itop-config-mgmt/3.2.0', + ], + 'mandatory' => false, + 'visible' => true, + + // Components + // + 'datamodel' => [ + + ], + 'webservice' => [ + + ], + 'data.struct' => [ + 'data/en_us.data.itop-flow-map.xml', + ], + 'data.sample' => [ + // add your sample data XML files here, + ], + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => [ + // Module specific settings go here, if any + ], + ] +); From c501280f5316159d6655096963c8cd963904111e Mon Sep 17 00:00:00 2001 From: v-dumas Date: Wed, 6 May 2026 11:55:25 +0200 Subject: [PATCH 06/10] =?UTF-8?q?N=C2=B07771=20-=20Align=20Dataflow=20opti?= =?UTF-8?q?on=20with=20product-itop=20installation.xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- datamodels/2.x/installation.xml | 11 ++++++++++- .../2.x/itop-flow-map/datamodel.itop-flow-map.xml | 2 +- datamodels/2.x/itop-flow-map/module.itop-flow-map.php | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/datamodels/2.x/installation.xml b/datamodels/2.x/installation.xml index 26335edf69..ec7fe5962a 100755 --- a/datamodels/2.x/installation.xml +++ b/datamodels/2.x/installation.xml @@ -54,6 +54,15 @@ true + + itop-flow-map + Application Data Flows + Modelize flows between your applications, with impacts analysis + + itop-flow-map + + true + itop-config-mgmt-storage Storage Devices @@ -80,7 +89,7 @@ itop-container-mgmt - false + true diff --git a/datamodels/2.x/itop-flow-map/datamodel.itop-flow-map.xml b/datamodels/2.x/itop-flow-map/datamodel.itop-flow-map.xml index 2b7dcf478e..01a2be920c 100644 --- a/datamodels/2.x/itop-flow-map/datamodel.itop-flow-map.xml +++ b/datamodels/2.x/itop-flow-map/datamodel.itop-flow-map.xml @@ -21,7 +21,7 @@ - + diff --git a/datamodels/2.x/itop-flow-map/module.itop-flow-map.php b/datamodels/2.x/itop-flow-map/module.itop-flow-map.php index a89b41d2fb..2ff936a417 100644 --- a/datamodels/2.x/itop-flow-map/module.itop-flow-map.php +++ b/datamodels/2.x/itop-flow-map/module.itop-flow-map.php @@ -10,7 +10,7 @@ SetupWebPage::AddModule( [ // Identification // - 'label' => 'Map applications data flows', + 'label' => 'Applications data flows', 'category' => 'business', // Setup From b529a61bc549d6b0eb930a3e360225dab387bd87 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Wed, 6 May 2026 13:50:58 +0200 Subject: [PATCH 07/10] :white_check_mark: Fix PHP code styles --- .../unitary-tests/sources/core/Email/EmailSymfonyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php index 2cefa18742..4d4b17e399 100644 --- a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php @@ -170,7 +170,7 @@ HTML; * Email clients display the last part they support, so text/plain must come first and text/html last. * * @see https://www.rfc-editor.org/rfc/rfc2046.html#section-5.1.4 - * @covers \Combodo\iTop\Core\Email\EmailSymfony::SetBody() + * @covers \Combodo\iTop\Core\Email\EmailSymfony::SetBody() * @since N°9574 */ public function testSetBodyAlternativePartOrderForHtmlEmailIsPlainThenHtml(): void From 51e7ef32dccf53ea12837c3888017026c6d1e8f7 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Thu, 7 May 2026 16:23:11 +0200 Subject: [PATCH 08/10] :construction_worker: Update action.yml --- .github/workflows/action.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index fdca1c0b9a..908f1a972f 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -1,9 +1,16 @@ name: Add PRs to Combodo PRs Dashboard on: - pull_request_target: + pull_request: types: - opened + issues: + types: + - opened + workflow_call: + secrets: + PR_AUTOMATICALLY_ADD_TO_PROJECT: + required: true jobs: add-to-project: @@ -31,23 +38,22 @@ jobs: run: | curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \ -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/labels \ + https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels \ -d '{"labels":["internal"]}' - name: Set PR author as assignee if member of the organization - if: env.is_member == 'true' + if: env.is_member == 'true' && github.event_name == 'pull_request' run: | curl -L \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \ - https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/assignees \ + https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assignees \ -d '{"assignees":["${{ github.event.pull_request.user.login }}"]}' env: is_member: ${{ env.is_member }} - - name: Add PR to the appropriate project - uses: actions/add-to-project@v1.0.2 + uses: actions/add-to-project@v2 with: project-url: ${{ env.project_url }} github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }} From ab6c50d52d9bbe79a9ece5cd6483ac8efd8c0b3b Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Thu, 7 May 2026 16:24:40 +0200 Subject: [PATCH 09/10] :construction_worker: Update action.yml --- .github/workflows/action.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 908f1a972f..2d51411d36 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -8,9 +8,6 @@ on: types: - opened workflow_call: - secrets: - PR_AUTOMATICALLY_ADD_TO_PROJECT: - required: true jobs: add-to-project: From 48e6203869a17d5ca9168e3a952e58ce7b3e2aef Mon Sep 17 00:00:00 2001 From: Benjamin DALSASS Date: Wed, 13 May 2026 10:58:59 +0200 Subject: [PATCH 10/10] =?UTF-8?q?N=C2=B09044=20-=20Application=20token=20a?= =?UTF-8?q?nd=20Impersonate=20(Log=20in=20as=20this=20user)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/userrights.class.inc.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index cc980e039d..ff88fbf638 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -658,6 +658,16 @@ abstract class User extends cmdbAbstractObject } return false; } + + /** + * Allow a user to be impersonated by another one (generally an administrator) in order to troubleshoot rights issues. + * + * @return bool + */ + public function CanBeImpersonated(): bool + { + return true; + } } /** @@ -1063,6 +1073,9 @@ class UserRights $oUser = self::FindUser($sLogin); if ($oUser) { $bRet = true; + if (!$oUser->CanBeImpersonated()) { + throw new Exception($oUser->GetName().' cannot be impersonated'); + } if (is_null(self::$m_oRealUser)) { // First impersonation self::$m_oRealUser = self::$m_oUser;