diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml
index fdca1c0b9a..2d51411d36 100644
--- a/.github/workflows/action.yml
+++ b/.github/workflows/action.yml
@@ -1,9 +1,13 @@
name: Add PRs to Combodo PRs Dashboard
on:
- pull_request_target:
+ pull_request:
types:
- opened
+ issues:
+ types:
+ - opened
+ workflow_call:
jobs:
add-to-project:
@@ -31,23 +35,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 }}
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;
diff --git a/datamodels/2.x/installation.xml b/datamodels/2.x/installation.xml
index 70d3866c17..b7c25da2fd 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,11 +89,20 @@
itop-container-mgmt
- false
+ true
+
+ itop-flow-map
+ Data flow
+ Map data flows between applications
+
+ itop-flow-map
+
+ false
+
diff --git a/datamodels/2.x/itop-config-mgmt/dictionaries/en.dict.itop-config-mgmt.php b/datamodels/2.x/itop-config-mgmt/dictionaries/en.dict.itop-config-mgmt.php
index 5691ff0956..3676218a08 100644
--- a/datamodels/2.x/itop-config-mgmt/dictionaries/en.dict.itop-config-mgmt.php
+++ b/datamodels/2.x/itop-config-mgmt/dictionaries/en.dict.itop-config-mgmt.php
@@ -167,7 +167,7 @@ Dict::Add('EN US', 'English', 'English', [
Dict::Add('EN US', 'English', 'English', [
'Class:Rack' => 'Rack',
- 'Class:Rack+' => 'A physical cabinet for Datacenter Devices and Chassis.',
+ 'Class:Rack+' => 'A physical cabinet for Datacenter Devices and Enclosures.',
'Class:Rack/ComplementaryName' => '%1$s - %2$s',
'Class:Rack/Attribute:nb_u' => 'Rack units',
'Class:Rack/Attribute:nb_u+' => '',
diff --git a/datamodels/2.x/itop-flow-map/data/en_us.data.itop-flow-map.xml b/datamodels/2.x/itop-flow-map/data/en_us.data.itop-flow-map.xml
new file mode 100644
index 0000000000..32f326d094
--- /dev/null
+++ b/datamodels/2.x/itop-flow-map/data/en_us.data.itop-flow-map.xml
@@ -0,0 +1,21 @@
+
+
+
+ http
+
+
+ https
+
+
+ ftp
+
+
+ sftp
+
+
+ AS2
+
+
+ X.400
+
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..01a2be920c
--- /dev/null
+++ b/datamodels/2.x/itop-flow-map/datamodel.itop-flow-map.xml
@@ -0,0 +1,750 @@
+
+
+
+
+ cmdbAbstractObject
+
+ bizmodel,searchable
+ false
+ dataflow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+ |
+
+ 1
+
+
+ 0
+ FunctionalCI:DataFlow:Outbound
+ SELECT DataFlow WHERE source_id=:this->id
+
+
+
+ |
+
+
+
+
+
+
+
+
+ id AND source_impact = 'yes']]>
+ id]]>
+ both
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+ -
+ 25
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..2ff936a417
--- /dev/null
+++ b/datamodels/2.x/itop-flow-map/module.itop-flow-map.php
@@ -0,0 +1,50 @@
+ '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
+ ],
+ ]
+);
diff --git a/sources/Application/TwigBase/Twig/Extension.php b/sources/Application/TwigBase/Twig/Extension.php
index 2c5ebeffae..18721f9af1 100644
--- a/sources/Application/TwigBase/Twig/Extension.php
+++ b/sources/Application/TwigBase/Twig/Extension.php
@@ -153,8 +153,12 @@ class Extension
return twig_array_filter($oTwigEnv, $array, $arrow);
}, ['needs_environment' => true]);
- // @since 3.3.0 N°8579
- // Filter to remove spaces between HTML tags, overwrite the deprecated core "spaceless" filter
+ /**
+ * Filter to remove spaces between HTML tags, overwrite the deprecated core "spaceless" filter
+ * Usage in twig: {% apply spaceless %}some html{% endapply %}
+ *
+ * @since 3.2.3 3.3.0 N°8579
+ */
$aFilters[] = new TwigFilter('spaceless', function (?string $content) {
return trim(preg_replace('/>\s+', '><', $content ?? ''));
}, ['is_safe' => ['html']]);
diff --git a/sources/Core/Email/EmailSymfony.php b/sources/Core/Email/EmailSymfony.php
index e397345c4d..fbb0120343 100644
--- a/sources/Core/Email/EmailSymfony.php
+++ b/sources/Core/Email/EmailSymfony.php
@@ -29,7 +29,9 @@ use Symfony\Component\CssSelector\Exception\ParseException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
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;
@@ -183,18 +185,7 @@ class EMailSymfony extends Email
$sDsn = sprintf('smtp://%s%s@%s%s', $sDsnUser, $sDsnPassword, $sDsnPort, $sEncQuery);
}
- $oTransport = Transport::fromDsn($sDsn);
-
- // Handle peer verification
- $oStream = $oTransport->getStream();
- $aOptions = $oStream->getStreamOptions();
- if (!$bVerifyPeer && array_key_exists('ssl', $aOptions)) {
- // Disable verification
- $aOptions['ssl']['verify_peer'] = false;
- $aOptions['ssl']['verify_peer_name'] = false;
- $aOptions['ssl']['allow_self_signed'] = true;
- }
- $oStream->setStreamOptions($aOptions);
+ $oTransport = $this->CreateSmtpTransport($sDsn, $bVerifyPeer);
$oMailer = new Mailer($oTransport);
break;
@@ -260,6 +251,36 @@ class EMailSymfony extends Email
}
}
+ /**
+ * Build and configure an SMTP transport from a DSN string.
+ *
+ * Extracted from {@see SendSynchronous} to make SSL option handling independently testable.
+ * When $bVerifyPeer is false, the ssl stream context options must be written unconditionally:
+ * with STARTTLS the connection starts unencrypted, so the 'ssl' key is absent from the stream
+ * options at construction time and only used later when stream_socket_enable_crypto() is called.
+ *
+ * @param string $sDsn Full Symfony Mailer DSN (smtp:// or smtps://)
+ * @param bool $bVerifyPeer Whether to verify the peer SSL certificate
+ *
+ * @return \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport
+ */
+ protected function CreateSmtpTransport(string $sDsn, bool $bVerifyPeer): EsmtpTransport
+ {
+ /** @var EsmtpTransport $oTransport */
+ $oTransport = Transport::fromDsn($sDsn);
+
+ $oStream = $oTransport->getStream();
+ $aOptions = $oStream->getStreamOptions();
+ if (!$bVerifyPeer) {
+ $aOptions['ssl']['verify_peer'] = false;
+ $aOptions['ssl']['verify_peer_name'] = false;
+ $aOptions['ssl']['allow_self_signed'] = true;
+ }
+ $oStream->setStreamOptions($aOptions);
+
+ return $oTransport;
+ }
+
/**
* Reprocess the body of the message (if it is an HTML message)
* to replace the URL of images based on attachments by a link
@@ -416,13 +437,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..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
@@ -1,6 +1,12 @@
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!