Compare commits

...

18 Commits

Author SHA1 Message Date
v-dumas
12083a909e N°9553 - Load structural data 2026-05-22 18:05:43 +02:00
v-dumas
2b9db73871 N°9553 - Helper for loading XML localized data during Setup 2026-05-22 16:13:19 +02:00
Vincent Dumas
ba2af7ed63 N°9604 - add "flowmaps" as a new neighbour (#914)
* Set DataFlow as FunctionalCI
* Use class name for Badge ID on DataFlow and Container classes including Typology
* Improve DataFlow summary
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-21 11:22:10 +02:00
Benjamin DALSASS
48e6203869 N°9044 - Application token and Impersonate (Log in as this user) 2026-05-13 11:14:58 +02:00
jf-cbd
ab6c50d52d 👷 Update action.yml 2026-05-07 16:24:40 +02:00
jf-cbd
51e7ef32dc 👷 Update action.yml 2026-05-07 16:23:11 +02:00
Molkobain
5ac675a587 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-05-06 14:53:01 +02:00
Molkobain
769afb2715 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-06 14:52:10 +02:00
Molkobain
b529a61bc5 Fix PHP code styles 2026-05-06 13:50:58 +02:00
v-dumas
c501280f53 N°7771 - Align Dataflow option with product-itop installation.xml 2026-05-06 11:55:25 +02:00
Vincent Dumas
e662370c32 N°7771 New module for DataFlow management (#905) 2026-05-06 11:28:21 +02:00
Molkobain
3c39c6d8d1 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-05-06 11:19:30 +02:00
Molkobain
a7d0262b21 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-06 11:18:36 +02:00
Molkobain
c56617abf5 N°8579 - Update PHPDoc 2026-05-06 10:55:39 +02:00
v-dumas
ff94639a61 N°9471 - Rack can contain Enclosure 2026-05-05 10:46:34 +02:00
Molkobain
7676115725 N°9584 - Fix "Unable to connect with STARTTLS" error when sending emails (#901)
* N°9584 - Fix "Unable to connect with STARTTLS" error when sending emails

* N°9584 - Refactor EMailSymfony transport to add unit tests
2026-05-05 09:51:09 +02:00
Molkobain
43ceaeb5a3 Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-05-04 16:42:30 +02:00
Håkon Harnes
7cac280b83 N°9574 - Fix CKEditor CSS displayed as part of the email message in Gmail (#898)
* fix(email): generate plain text before inlining HTML CSS

* N°9574 - Add unit tests

* Apply suggestions from code review

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2026-05-04 16:33:38 +02:00
18 changed files with 1388 additions and 70 deletions

View File

@@ -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 }}

View File

@@ -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;

View File

@@ -54,6 +54,15 @@
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-flow-map</extension_code>
<title>Application Data Flows</title>
<description>Modelize flows between your applications, with impacts analysis</description>
<modules type="array">
<module>itop-flow-map</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-storage</extension_code>
<title>Storage Devices</title>
@@ -80,11 +89,20 @@
<modules type="array">
<module>itop-container-mgmt</module>
</modules>
<default>false</default>
<default>true</default>
</choice>
</options>
</sub_options>
</choice>
<choice>
<extension_code>itop-flow-map</extension_code>
<title>Data flow</title>
<description>Map data flows between applications</description>
<modules type="array">
<module>itop-flow-map</module>
</modules>
<default>false</default>
</choice>
</options>
</step>
<step>

View File

@@ -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+' => '',

View File

@@ -1480,19 +1480,19 @@
<cell id="1" _delta="must_exist">
<rank>1</rank>
<dashlets>
<dashlet id="container_43" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerApplication" xsi:type="DashletBadge" _delta="define">
<rank>5</rank>
<class>ContainerApplication</class>
</dashlet>
<dashlet id="container_44" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerHost" xsi:type="DashletBadge" _delta="define">
<rank>6</rank>
<class>ContainerHost</class>
</dashlet>
<dashlet id="container_45" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerCluster" xsi:type="DashletBadge" _delta="define">
<rank>7</rank>
<class>ContainerCluster</class>
</dashlet>
<dashlet id="container_46" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerImage" xsi:type="DashletBadge" _delta="define">
<rank>8</rank>
<class>ContainerImage</class>
</dashlet>
@@ -1507,11 +1507,11 @@
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="container_21" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerType" xsi:type="DashletBadge" _delta="define">
<rank>21</rank>
<class>ContainerType</class>
</dashlet>
<dashlet id="container_22" xsi:type="DashletBadge" _delta="define">
<dashlet id="ContainerImageType" xsi:type="DashletBadge" _delta="define">
<rank>22</rank>
<class>ContainerImageType</class>
</dashlet>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<DataFlowType alias="DataFlowType" id="1">
<name>http</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="2">
<name>https</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="3">
<name>ftp</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="4">
<name>sftp</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="5">
<name>AS2</name>
</DataFlowType>
<DataFlowType alias="DataFlowType" id="6">
<name>X.400</name>
</DataFlowType>
</Set>

View File

@@ -0,0 +1,615 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFlow" _delta="define">
<parent>FunctionalCI</parent>
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dataflow</db_table>
<style>
<icon>images/icons8-sorting-arrows-horizontal.svg</icon>
</style>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
<complementary_attributes>
<attribute id="source_id"/>
<attribute id="destination_id"/>
</complementary_attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
<attribute id="destination_id"/>
<attribute id="org_id"/>
<attribute id="source_id"/>
<attribute id="dataflowtype_id"/>
</attributes>
</reconciliation>
<obsolescence>
<condition>status='inactive'</condition>
</obsolescence>
<fields_semantic>
<state_attribute>status</state_attribute>
</fields_semantic>
</properties>
<fields>
<field id="source_id" xsi:type="AttributeExternalKey">
<sql>source_id</sql>
<filter><![CDATA[SELECT FunctionalCI WHERE finalclass != 'DataFlow']]></filter>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>FunctionalCI</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="source_impact" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="yes">
<code>yes</code>
<rank>10</rank>
</value>
<value id="no">
<code>no</code>
<rank>20</rank>
</value>
</values>
<sql>source_impact</sql>
<default_value>yes</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>radio_horizontal</display_style>
</field>
<field id="destination_id" xsi:type="AttributeExternalKey">
<sql>destination_id</sql>
<filter><![CDATA[SELECT FunctionalCI WHERE finalclass != 'DataFlow']]></filter>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>FunctionalCI</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="destination_impact" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="yes">
<code>yes</code>
<rank>10</rank>
</value>
<value id="no">
<code>no</code>
<rank>20</rank>
</value>
</values>
<sql>destination_impact</sql>
<default_value>no</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>radio_horizontal</display_style>
</field>
<field id="dataflowtype_id" xsi:type="AttributeExternalKey">
<sql>dataflowtype_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>DataFlowType</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
<field id="status" xsi:type="AttributeEnum">
<sql>status</sql>
<values>
<value id="active">
<code>active</code>
<rank>10</rank>
<style>
<main_color>$ibo-lifecycle-active-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-active-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
<value id="inactive">
<code>inactive</code>
<rank>20</rank>
<style>
<main_color>$ibo-lifecycle-inactive-state-primary-color</main_color>
<complementary_color>$ibo-lifecycle-inactive-state-secondary-color</complementary_color>
<decoration_classes/>
</style>
</value>
</values>
<sort_type>label</sort_type>
<default_value>active</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>list</display_style>
<tracking_level>all</tracking_level>
</field>
<field id="execution_frequency" xsi:type="AttributeEnum">
<sort_type>rank</sort_type>
<values>
<value id="realtime">
<code>realtime</code>
<rank>10</rank>
</value>
<value id="ondemand">
<code>ondemand</code>
<rank>20</rank>
</value>
<value id="hourly">
<code>hourly</code>
<rank>30</rank>
</value>
<value id="daily">
<code>daily</code>
<rank>40</rank>
</value>
<value id="weekly">
<code>weekly</code>
<rank>50</rank>
</value>
<value id="monthly">
<code>monthly</code>
<rank>60</rank>
</value>
<value id="yearly">
<code>yearly</code>
<rank>70</rank>
</value>
</values>
<sql>execution_frequency</sql>
<default_value>daily</default_value>
<is_null_allowed>false</is_null_allowed>
<display_style>list</display_style>
</field>
</fields>
<event_listeners>
<event_listener id="evtCheckSourceAndDestination">
<event>EVENT_DB_CHECK_TO_WRITE</event>
<rank>10</rank>
<callback>evtCheckSourceAndDestination</callback>
</event_listener>
</event_listeners>
<methods>
<method id="evtCheckSourceAndDestination" _delta="define">
<comment> /**
* Ensure that the source and destination of a data flow are not DataFlow themselves
*
*/</comment>
<static>false</static>
<access>public</access>
<type>EventListener</type>
<code><![CDATA[ public function evtCheckSourceAndDestination(Combodo\iTop\Service\Events\EventData $oEventData)
{
$oSource = MetaModel::GetObject(FunctionalCI::class, $this->Get('source_id'), false, true);
$oDestination = MetaModel::GetObject(FunctionalCI::class, $this->Get('destination_id'), false, true);
if ($oSource instanceof DataFlow) {
$this->AddCheckIssue(Dict::Format('Class:DataFlow/Error:CheckSource', $oSource->GetName()));
}
if ($oDestination instanceof DataFlow) {
$this->AddCheckIssue(Dict::Format('Class:DataFlow/Error:CheckDestination', $oDestination->GetName()));
}
} ]]></code>
</method>
</methods>
<presentation>
<list>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="dataflowtype_id">
<rank>40</rank>
</item>
<item id="business_criticity">
<rank>50</rank>
</item>
</items>
</list>
<search>
<items>
<item id="org_id">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="status">
<rank>40</rank>
</item>
</items>
</search>
<details>
<items>
<item id="col:col1">
<items>
<item id="fieldset:ConfigMgmt:baseinfo">
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="org_id">
<rank>20</rank>
</item>
<item id="status">
<rank>30</rank>
</item>
<item id="business_criticity">
<rank>40</rank>
</item>
</items>
<rank>10</rank>
</item>
<item id="fieldset:DataFlow:moreinfo">
<items>
<item id="source_id">
<rank>10</rank>
</item>
<item id="source_impact">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="destination_impact">
<rank>40</rank>
</item>
<item id="dataflowtype_id">
<rank>50</rank>
</item>
<item id="execution_frequency">
<rank>60</rank>
</item>
</items>
<rank>20</rank>
</item>
</items>
<rank>10</rank>
</item>
<item id="col:col2">
<items>
<item id="fieldset:ConfigMgmt:dates">
<items>
<item id="move2production">
<rank>10</rank>
</item>
</items>
<rank>10</rank>
</item>
<item id="fieldset:ConfigMgmt:otherinfo">
<items>
<item id="description">
<rank>10</rank>
</item>
<item id="groups_list">
<rank>20</rank>
</item>
</items>
<rank>20</rank>
</item>
</items>
<rank>20</rank>
</item>
<item id="contacts_list">
<rank>70</rank>
</item>
<item id="documents_list">
<rank>80</rank>
</item>
</items>
</details>
<default_search>
<items>
<item id="org_id">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="dataflowtype_id">
<rank>40</rank>
</item>
<item id="status">
<rank>50</rank>
</item>
</items>
</default_search>
<summary>
<items>
<item id="business_criticity">
<rank>10</rank>
</item>
<item id="source_id">
<rank>20</rank>
</item>
<item id="destination_id">
<rank>30</rank>
</item>
<item id="execution_frequency">
<rank>40</rank>
</item>
</items>
</summary>
</presentation>
<relations>
<relation id="impacts">
<neighbours>
<neighbour id="functionalci">
<query_down><![CDATA[SELECT FunctionalCI WHERE :this->destination_impact = 'yes' AND id = :this->destination_id]]></query_down>
<query_up><![CDATA[SELECT DataFlow AS f JOIN FunctionalCI AS ci ON f.destination_id = ci.id WHERE f.destination_impact = 'yes' AND ci.id=:this->id]]></query_up>
<direction>both</direction>
</neighbour>
<neighbour id="contact">
<attribute>contacts_list</attribute>
<direction>down</direction>
</neighbour>
</neighbours>
</relation>
<relation id="dataflows">
<neighbours>
<neighbour id="functionalci">
<query_down><![CDATA[SELECT FunctionalCI WHERE id = :this->destination_id]]></query_down>
<query_up><![CDATA[SELECT DataFlow AS f WHERE f.destination_id = :this->id]]></query_up>
<direction>both</direction>
</neighbour>
</neighbours>
</relation>
</relations>
</class>
<class id="DataFlowType" _delta="define">
<parent>Typology</parent>
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dataflowtype</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<uniqueness_rules>
<rule id="name">
<attributes>
<attribute id="name"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
</properties>
<fields/>
<methods/>
<presentation>
<list>
<items>
<item id="finalclass">
<rank>10</rank>
</item>
</items>
</list>
<search>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="FunctionalCI" _delta="must_exist">
<fields>
<field id="dataflows" xsi:type="AttributeDashboard" _delta="define">
<is_user_editable>true</is_user_editable>
<definition>
<layout>DashboardLayoutTwoCols</layout>
<title>FunctionalCI:DataFlow:Title</title>
<auto_reload>
<enabled>false</enabled>
<interval>300</interval>
</auto_reload>
<cells>
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="DataFlow_Inbound" xsi:type="DashletObjectList">
<rank>0</rank>
<title>FunctionalCI:DataFlow:Inbound</title>
<query>SELECT DataFlow WHERE destination_id=:this->id</query>
<menu>true</menu>
</dashlet>
</dashlets>
</cell>
<cell id="1">
<rank>1</rank>
<dashlets>
<dashlet id="DataFlow_Outbound" xsi:type="DashletObjectList">
<rank>0</rank>
<title>FunctionalCI:DataFlow:Outbound</title>
<query>SELECT DataFlow WHERE source_id=:this->id</query>
<menu>true</menu>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</field>
</fields>
<relations>
<relation id="impacts">
<neighbours>
<neighbour id="flow" _delta="define">
<query_down><![CDATA[SELECT DataFlow WHERE source_id = :this->id AND source_impact = 'yes']]></query_down>
<query_up><![CDATA[SELECT FunctionalCI AS ci JOIN DataFlow AS f ON f.source_id = ci.id WHERE f.source_impact = 'yes' AND f.id = :this->id]]></query_up>
<direction>both</direction>
</neighbour>
</neighbours>
</relation>
<relation id="dataflows" _delta="define">
<neighbours>
<neighbour id="flow">
<query_down><![CDATA[SELECT DataFlow WHERE source_id = :this->id]]></query_down>
<query_up><![CDATA[SELECT FunctionalCI AS ci JOIN DataFlow AS f ON f.source_id = ci.id WHERE f.id = :this->id]]></query_up>
<direction>both</direction>
</neighbour>
</neighbours>
</relation>
</relations>
</class>
<class id="ApplicationSolution" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="DatabaseSchema" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="DBServer" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="Middleware" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="MiddlewareInstance" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="WebApplication" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="WebServer" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
<class id="OtherSoftware" _delta="must_exist">
<presentation>
<details>
<items>
<item id="dataflows" _delta="define">
<rank>25</rank>
</item>
</items>
</details>
</presentation>
</class>
</classes>
<menus>
<menu id="ConfigManagementOverview" xsi:type="DashboardMenuNode" _delta="must_exist">
<definition>
<cells>
<cell id="3" delta="if_exists">
<dashlets>
<dashlet id="DataFlow" xsi:type="DashletBadge" _delta="define">
<rank>20</rank>
<class>DataFlow</class>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</menu>
<menu id="Typology" xsi:type="DashboardMenuNode" _delta="must_exist">
<definition>
<cells>
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="DataFlowType" xsi:type="DashletBadge" _delta="define">
<rank>23</rank>
<class>DataFlowType</class>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</menu>
</menus>
<user_rights>
<groups>
<group id="Configuration">
<classes>
<class id="DataFlow" _delta="define_if_not_exists"/>
</classes>
</group>
</groups>
<profiles>
</profiles>
</user_rights>
</itop_design>

View File

@@ -0,0 +1,91 @@
<?php
/**
* Module combodo-flow-map
*
* @copyright Copyright (C) 2013 XXXXX
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('EN US', 'English', 'English', [
'Relation:dataflows/Description' => 'DataFlows between CIs',
'Relation:dataflows/DownStream' => 'Outbound flows...',
'Relation:dataflows/DownStream+' => 'Outbound flows map from',
'Relation:dataflows/UpStream' => 'Inbound flows...',
'Relation:dataflows/UpStream+' => 'Inbound flows map to',
'Class:FunctionalCI/Attribute:dataflows' => '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:moreinfo' => 'Flow specifics',
'Class:DataFlow' => 'Flow',
'Class:DataFlow+' => 'For application flow for example',
'Class:DataFlow/ComplementaryName' => '%1$s - %2$s',
'Class:DataFlow/Attribute:name' => 'Name',
'Class:DataFlow/Attribute:name+' => 'Identify the transferred data flow',
'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:status' => 'Status',
'Class:DataFlow/Attribute:status+' => '',
'Class:DataFlow/Attribute:status/Value:active' => 'active',
'Class:DataFlow/Attribute:status/Value:inactive' => 'inactive',
'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+' => 'Eg: technical specifications, runbooks, etc.',
'Class:DataFlow/Attribute:contacts_list+' => 'Eg: flow owner, technical support, etc.',
'Class:DataFlow/Error:CheckSource' => 'The source of a data flow cannot be a data flow itself. Choose another source CI than %1$s',
'Class:DataFlow/Error:CheckDestination' => 'The destination of a data flow cannot be a data flow itself. Choose another destination CI than %1$s',
'Class:DataFlowType' => 'Data Flow Type',
'Class:DataFlowType+' => 'Typology of Data Flow',
/*
'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',
*/
]);

View File

@@ -0,0 +1,91 @@
<?php
/**
* Module combodo-flow-map
*
* @copyright Copyright (C) 2013 XXXXX
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('FR FR', 'French', 'Français', [
'Relation:dataflows/Description' => 'Flux de données entre CIs',
'Relation:dataflows/DownStream' => 'Flux sortants...',
'Relation:dataflows/DownStream+' => 'Carte des flux sortants depuis',
'Relation:dataflows/UpStream' => 'Flux entrants...',
'Relation:dataflows/UpStream+' => 'Carte des flux entrants vers',
'Class:FunctionalCI/Attribute:dataflows' => '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: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 ou plus généralement entre CIs.',
'Class:DataFlow/ComplementaryName' => '%1$s - %2$s',
'Class:DataFlow/Attribute:name' => 'Nom',
'Class:DataFlow/Attribute:name+' => 'Identifie le flux de donné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:status' => 'Etat',
'Class:DataFlow/Attribute:status+' => '',
'Class:DataFlow/Attribute:status/Value:active' => 'actif',
'Class:DataFlow/Attribute:status/Value:inactive' => 'inactif',
'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+' => 'Eg: technical specifications, runbooks, etc.',
'Class:DataFlow/Attribute:contacts_list+' => 'Eg: flow owner, technical support, etc.',
'Class:DataFlow/Error:CheckSource' => 'La source d\'un flux de données ne peut pas être un flux de données elle-même. Choisissez un autre CI source que %1$s',
'Class:DataFlow/Error:CheckDestination' => 'La destination d\'un flux de données ne peut pas être un flux de données elle-même. Choisissez un autre CI destination que %1$s',
'Class:DataFlowType' => 'Type de flux',
'Class:DataFlowType+' => 'Typologie des flux de données',
/*
'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',
*/
]);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="mv_DwPz_GcV~datTQ_sP3a" x1="27.258" x2="38.501" y1="18.189" y2="44.314" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3a)" d="M14,41.19V37h14c0.552,0,1-0.448,1-1v-4c0-0.552-0.448-1-1-1H14v-4.19 c0-0.72-0.87-1.08-1.379-0.571L5.92,32.939c-0.586,0.586-0.586,1.536,0,2.121l6.701,6.701C13.13,42.271,14,41.91,14,41.19z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3b" x1="32.674" x2="34.456" y1="9.581" y2="13.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3b)" d="M35,36v-4c0-0.552,0.448-1,1-1l0,0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1l0,0 C35.448,37,35,36.552,35,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3c" x1="32.674" x2="34.456" y1="5.581" y2="9.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3c)" d="M39,36v-4c0-0.552,0.448-1,1-1l0,0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1l0,0 C39.448,37,39,36.552,39,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3d" x1="32.674" x2="34.456" y1="13.581" y2="17.722" gradientTransform="rotate(90 23.5 24)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3d)" d="M31,36v-4c0-0.552,0.448-1,1-1h0c0.552,0,1,0.448,1,1v4c0,0.552-0.448,1-1,1h0 C31.448,37,31,36.552,31,36z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3e" x1="551.258" x2="562.501" y1="-252.291" y2="-226.167" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3e)" d="M33,7.81V12H19c-0.552,0-1,0.448-1,1v4c0,0.552,0.448,1,1,1h14v4.19 c0,0.72,0.87,1.08,1.379,0.571l6.701-6.701c0.586-0.586,0.586-1.536,0-2.121l-6.701-6.701C33.87,6.729,33,7.09,33,7.81z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3f" x1="556.674" x2="558.456" y1="-260.899" y2="-256.759" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3f)" d="M12,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C11.552,12,12,12.448,12,13z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3g" x1="556.674" x2="558.456" y1="-264.899" y2="-260.759" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3g)" d="M8,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C7.552,12,8,12.448,8,13z"/><linearGradient id="mv_DwPz_GcV~datTQ_sP3h" x1="556.674" x2="558.456" y1="-256.899" y2="-252.758" gradientTransform="rotate(-90 421.24 151.26)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1ea2e4"/><stop offset="1" stop-color="#32bdef"/></linearGradient><path fill="url(#mv_DwPz_GcV~datTQ_sP3h)" d="M16,13v4c0,0.552-0.448,1-1,1h0c-0.552,0-1-0.448-1-1v-4c0-0.552,0.448-1,1-1h0 C15.552,12,16,12.448,16,13z"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,50 @@
<?php
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-flow-map/3.3.0',
[
// Identification
//
'label' => '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
],
]
);

View File

@@ -99,6 +99,22 @@ if (!class_exists('StructureInstaller')) {
*/
public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion)
{
// Load localized structural data: contact types and contract types
static::LoadLocalizedData(
$sPreviousVersion,
$sCurrentVersion,
$oConfiguration,
'3.3.0',
dirname(__FILE__)."/data/{{language_code}}.data.itop-contacttype.xml"
);
static::LoadLocalizedData(
$sPreviousVersion,
$sCurrentVersion,
$oConfiguration,
'3.3.0',
dirname(__FILE__)."/data/{{language_code}}.data.itop-contracttype.xml"
);
// Default language will be used for actions
// Note: There is a issue when upgrading, default language cannot be retrieved from the passed configuration, we have to read it from the disk
if (utils::IsNullOrEmptyString($sPreviousVersion)) {

View File

@@ -56,46 +56,13 @@ class TicketsInstaller extends ModuleInstallerAPI
if (!MetaModel::IsValidClass($oTrigger->Get('target_class'))) {
$oTrigger->DBDelete();
}
} catch (Exception $e) {
}
catch (Exception $e) {
utils::EnrichRaisedException($oTrigger, $e);
}
}
// It's not very clear if it make sense to test a particular version,
// as the loading mechanism checks object existence using reconc_keys
// and do not recreate them, nor update existing.
// Without test, new entries added to the data files, would be automatically loaded
if (($sPreviousVersion === '') ||
(version_compare($sPreviousVersion, $sCurrentVersion, '<')
&& version_compare($sPreviousVersion, '3.0.0', '<'))) {
$oDataLoader = new XMLDataLoader();
CMDBObject::SetTrackInfo("Initialization TicketsInstaller");
$oMyChange = CMDBObject::GetCurrentChange();
$sLang = null;
// - Try to get app. language from configuration fil (app. upgrade)
$sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE;
if (file_exists($sConfigFileName)) {
$oFileConfig = new Config($sConfigFileName);
if (is_object($oFileConfig)) {
$sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage()));
}
}
// - I still no language, get the default one
if (null === $sLang) {
$sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage()));
}
$sFileName = dirname(__FILE__)."/data/{$sLang}.data.itop-tickets.xml";
SetupLog::Info("Searching file: $sFileName");
if (!file_exists($sFileName)) {
$sFileName = dirname(__FILE__)."/data/en_us.data.itop-tickets.xml";
}
SetupLog::Info("Loading file: $sFileName");
$oDataLoader->StartSession($oMyChange);
$oDataLoader->LoadFile($sFileName, false, true);
$oDataLoader->EndSession();
}
// Load localized structural data: predefined query phrases for notifications
static::LoadLocalizedData($sPreviousVersion, $sCurrentVersion, $oConfiguration, '3.0.0', dirname(__FILE__)."/data/{{language_code}}.data.itop-tickets.xml");
}
}

View File

@@ -309,4 +309,86 @@ abstract class ModuleInstallerAPI
CMDBSource::CacheReset($sOrigTable);
}
/**
* @param string $sPreviousVersion The previous version of the module (empty string will force the loading)
* @param string $sCurrentVersion The current version of the module
* @param \Config $oConfiguration
* @param string $sFirstLoadingVersion The first module version for which the data loading should be performed (e.g. '3.0.0')
* @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public static function LoadLocalizedData(string $sPreviousVersion, string $sCurrentVersion, Config $oConfiguration, string $sFirstLoadingVersion, string $sFilePattern): void
{
// It's not very clear if it makes sense to test a particular version,
// as the loading mechanism checks object existence using reconc_keys
// and do not recreate them, nor update existing.
// Without test, new entries added to the data files, would be automatically loaded
if (($sPreviousVersion === '') ||
(version_compare($sPreviousVersion, $sCurrentVersion, '<')
&& version_compare($sPreviousVersion, $sFirstLoadingVersion, '<'))) {
// Note: There is an issue when upgrading, default language cannot be retrieved from the passed configuration, we have to read it from the disk
if (utils::IsNullOrEmptyString($sPreviousVersion)) {
// Fresh install
$sDefaultLanguage = $oConfiguration->GetDefaultLanguage();
} else {
// Upgrade
$sDefaultLanguage = utils::GetConfig(true)->GetDefaultLanguage();
}
$sFileName = self::GetLocalizedFileName($sDefaultLanguage, $sFilePattern);
if ($sFileName !== '') {
SetupLog::Info("Loading file: $sFileName");
self::XMLFileLoad($sFileName);
}
}
}
/**
* @param array|string $sFileName
* @param \XMLDataLoader $oDataLoader
*
* @return void
* @throws \Exception
*/
public static function XMLFileLoad(string $sFileName): void
{
if (file_exists($sFileName)) {
$oDataLoader = new XMLDataLoader();
CMDBObject::SetTrackInfo("Loading XML data from $sFileName");
$oMyChange = CMDBObject::GetCurrentChange();
SetupLog::Info("Loading file: $sFileName");
$oDataLoader->StartSession($oMyChange);
$oDataLoader->LoadFile($sFileName, false, true);
$oDataLoader->EndSession();
}
}
/**
* @param string $sLanguage The language code to use for localization (e.g. 'EN US')
* @param string $sFilePattern The full path+name of the file to localize, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
*
* @return string The localized file name if found, or an empty string if not found
* @throws \ConfigException
* @throws \CoreException
*/
public static function GetLocalizedFileName($sLanguage, string $sFilePattern): string
{
$sLang = str_replace(' ', '_', strtolower($sLanguage));
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
if (!file_exists($sFileName)) {
$sLang = 'en_us';
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
}
if (file_exists($sFileName)) {
return $sFileName;
} else {
SetupLog::Warning("No data file matching the pattern $sFilePattern and language_code $sLang was found.");
return '';
}
}
}

View File

@@ -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']]);

View File

@@ -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;

View File

@@ -4,11 +4,12 @@ namespace Combodo\iTop\Test\UnitTest\Setup;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Config;
use MetaModel;
use ModuleInstallerAPI;
/**
* Class ModuleInstallerAPITest
* Class ModuleInstallerAPI
*
* @covers ModuleInstallerAPI
*
@@ -282,4 +283,105 @@ SQL
$this->assertEquals($sOrigValue, $sDstValue, "Data was not moved as expected");
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_LoadsOnFirstInstall(): void
{
// Given
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_FirstInstall_', 'fr_fr');
$this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
$this->CreateLocalizedDataFile($sTmpDir, "fr_fr", $sOrgName);
// When no previous version, and current version higher than the first loading version
ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern);
// Then data loaded
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 1);
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_DoesNotLoadWhenVersionConditionIsNotMet(): void
{
// Given
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_NoLoad_', 'en_us');
$this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
// When a previous version that is lower than the first loading version, but higher or equal to the current version
ModuleInstallerAPI::LoadLocalizedData('3.0.0', '3.1.0', $oConfig, '3.0.0', $sPattern);
// Then no data loaded
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_FallbacksToEnUsWhenLanguageFileIsMissing(): void
{
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_Fallback_', 'fr_fr');
// Intentionally create ONLY en_us file
$this->CreateLocalizedDataFile($sTmpDir, 'en_us', $sOrgName);
// When loading localized data in fr_fr, but only en_us file exists
ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern);
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 0);
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 1);
}
/**
* Prepare common context for LoadLocalizedData tests.
*
* @return array{0: Config, 1: string, 2: string, 3: string, 4: string}
*/
private function PrepareLocalizedDataTestContext(string $sOrgNamePrefix, string $sLanguage): array
{
$oConfig = MetaModel::GetConfig();
$oConfig->SetDefaultLanguage($sLanguage);
$this->assertNotNull($oConfig);
$sOrgName = $sOrgNamePrefix.uniqid();
$sTmpDir = static::CreateTmpdir();
$this->aFileToClean[] = $sTmpDir;
$sPattern = $sTmpDir.DIRECTORY_SEPARATOR.'data.{{language_code}}.xml';
return [$oConfig, $sOrgName, $sTmpDir, $sPattern];
}
private function CreateLocalizedDataFile(string $sDir, string $sLang, string $sOrgName): string
{
$sFilePath = $sDir.DIRECTORY_SEPARATOR.'data.'.$sLang.'.xml';
file_put_contents($sFilePath, $this->BuildOrganizationXml($sOrgName, $sLang));
return $sFilePath;
}
private function BuildOrganizationXml(string $sOrgName, string $sLang): string
{
$iId = random_int(100000, 999999);
$sOrgNameXml = htmlspecialchars($sOrgName, ENT_XML1);
return <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<Organization alias="Organization" id="{$iId}">
<name>{$sOrgNameXml}</name>
<code>{$sLang}</code>
<status>active</status>
</Organization>
</Set>
XML;
}
private function AssertOrganizationCountByName(string $sOrgName, string $sLanguage, int $iExpectedCount): void
{
$sOrgTable = MetaModel::DBGetTable('Organization');
$iCount = (int) CMDBSource::QueryToScalar(
"SELECT COUNT(*) FROM `{$sOrgTable}` WHERE `name` = ".CMDBSource::Quote($sOrgName)." AND `code` = ".CMDBSource::Quote($sLanguage)
);
$this->assertEquals($iExpectedCount, $iCount);
}
}

View File

@@ -1,6 +1,12 @@
<?php
use Combodo\iTop\Core\Email\EMailSymfony;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
class EmailSymfonyTest extends ItopTestCase
{
@@ -135,4 +141,221 @@ HTML;
$this->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('<p>Hello there!</p>', '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('<style>', $sPlainText, 'Style tag must not appear in plain text');
$this->assertStringNotContainsString('color:', $sPlainText, 'CSS color rule must not appear in plain text');
$this->assertStringNotContainsString('font-size:', $sPlainText, 'CSS font-size rule must not appear in plain text');
$this->assertStringNotContainsString('@media', $sPlainText, 'CSS @media rule must not appear in plain text');
$this->assertStringContainsString('Hello there!', $sPlainText, 'Actual content must be preserved in plain text');
}
/**
* The HTML part must contain the body content and the CSS inlined by Emogrifier.
* This guards against regressions where the wrong body (e.g. the plain-text version)
* would end up in the HTML part.
*
* @covers \Combodo\iTop\Core\Email\EmailSymfony::SetBody()
* @since N°9574
*/
public function testSetBodyHtmlPartContainsBodyAndInlinedCss(): void
{
$oEmail = new EMailSymfony();
$oEmail->SetBody('<html><body><p>Hello there!</p></body></html>', 'text/html', 'p { color: red; }');
$aParts = $this->GetAlternativePartsFromHtmlEmail($oEmail);
$oHtmlPart = null;
foreach ($aParts as $oPart) {
if ($oPart instanceof TextPart && $oPart->getMediaSubtype() === 'html') {
$oHtmlPart = $oPart;
break;
}
}
$this->assertNotNull($oHtmlPart, 'No text/html part found in the message');
$sHtmlContent = $oHtmlPart->getBody();
$this->assertStringContainsString('Hello there!', $sHtmlContent, 'HTML part must preserve the original text content');
$this->assertStringContainsString('color: red', $sHtmlContent, 'HTML part must contain the CSS inlined by Emogrifier');
}
/**
* With inline images, SetBody() wraps the AlternativePart in a RelatedPart.
* The AlternativePart must still be correctly ordered (plain first, HTML last)
* and the plain-text part must not contain CSS.
*
* @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 testSetBodyWithInlineImagesHasCorrectPartStructure(): void
{
// Anonymous subclass so we can inject a fake inline image part without a real inline image in DB
$oEmail = new class () extends EMailSymfony {
protected function EmbedInlineImages(string &$sBody): array
{
return [new DataPart('fake-image-data', 'image.png', 'image/png')];
}
};
$oEmail->SetBody('<html><head><style>p { color: red; }</style></head><body><p>Hello there!</p></body></html>', '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 [
'<style> tag in HTML, no custom styles' => [
'<html><head><style>body { color: red; font-size: 12px; } @media print { p { color: black; } }</style></head><body><p>Hello there!</p></body></html>',
null,
],
'<style> tag in HTML with custom styles' => [
'<html><head><style>body { color: red; font-size: 12px; } @media print { p { color: black; } }</style></head><body><p>Hello there!</p></body></html>',
$sCustomStyles,
],
'custom styles only, no <style> tag' => [
'<html><body><p>Hello there!</p></body></html>',
$sCustomStyles,
],
];
}
/**
* @dataProvider provideCreateSmtpTransportSslOptions
*/
public function testCreateSmtpTransportSslOptions(string $sDsn, bool $bVerifyPeer, array $aExpectedSslOptions): void
{
$oEmail = new EMailSymfony();
/** @var EsmtpTransport $oTransport */
$oTransport = $this->InvokeNonPublicMethod(EMailSymfony::class, 'CreateSmtpTransport', $oEmail, [$sDsn, $bVerifyPeer]);
$aActualSslOptions = $oTransport->getStream()->getStreamOptions()['ssl'] ?? [];
$this->assertSame($aExpectedSslOptions, $aActualSslOptions);
}
public function provideCreateSmtpTransportSslOptions(): array
{
$aDisabledVerification = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
return [
// Regression scenario (N°9584): STARTTLS starts the connection unencrypted, so the 'ssl' key
// is absent from stream options at construction time. verify_peer=false must still be applied.
'STARTTLS, verify_peer=false' => [
'smtp://localhost:587?encryption=starttls',
false,
$aDisabledVerification,
],
'implicit TLS (smtps), verify_peer=false' => [
'smtps://localhost:465',
false,
$aDisabledVerification,
],
'plain SMTP, verify_peer=false' => [
'smtp://localhost:25',
false,
$aDisabledVerification,
],
// Default behavior: verify_peer=true must leave stream options untouched (empty).
'STARTTLS, verify_peer=true (default)' => [
'smtp://localhost:587?encryption=starttls',
true,
[],
],
'implicit TLS (smtps), verify_peer=true (default)' => [
'smtps://localhost:465',
true,
[],
],
'plain SMTP, verify_peer=true (default)' => [
'smtp://localhost:25',
true,
[],
],
];
}
}