N°918 - Translate placeholders in notifications (#506)

- Localization of date & time formats
- Use of DataLocalizer (if present)
- All placeholders can be used in the uploaded HTML template as well as in the notification "message"
This commit is contained in:
Denis
2023-06-13 09:51:32 +02:00
committed by GitHub
parent 58a20e9a11
commit fed149dc66
6 changed files with 530 additions and 118 deletions

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
/**
* Persistent classes (internal): user defined actions
@@ -225,7 +226,22 @@ class ActionEmail extends ActionNotification
* @since 3.0.1
*/
const ENUM_HEADER_NAME_REFERENCES = 'References';
/**
* @var string
* @since 3.1.0
*/
const TEMPLATE_BODY_CONTENT = '$content$';
/**
* Wraps the 'body' of the message for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied
* @var string
* @since 3.1.0
*/
const CONTENT_HIGHLIGHT = '<div style="border:2px dashed #6800ff;position:relative;padding:2px;margin-top:14px;"><div style="background-color:#6800ff;color:#fff;font-family:Courier New, sans-serif;font-size:14px;line-height:16px;padding:3px;display:block;position:absolute;top:-22px;right:0;">$content$</div>%s</div>';
/**
* Wraps a placeholder of the email's body for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied
* @var string
*/
const FIELD_HIGHLIGHT = '<span style="background-color:#6800ff;color:#fff;font-size:smaller;font-family:Courier New, sans-serif;padding:2px;">\\$$1\\$</span>';
/**
* @inheritDoc
*/
@@ -257,6 +273,10 @@ class ActionEmail extends ActionNotification
MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values" => null, "sql" => "subject", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values" => null, "sql" => "body", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values" => new ValueSetEnum('low,normal,high'), "sql" => "importance", "default_value" => 'normal', "is_null_allowed" => false, "depends_on" => array())));
MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeBlob("html_template", array("is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false)));
MetaModel::Init_AddAttribute(new AttributeEnum("ignore_notify", array("allowed_values" => new ValueSetEnum('yes,no'), "sql" => "ignore_notify", "default_value" => 'yes', "is_null_allowed" => false, "depends_on" => array())));
// Display lists
// - Attributes to be displayed for the complete details
@@ -266,8 +286,10 @@ class ActionEmail extends ActionNotification
0 => 'name',
1 => 'description',
2 => 'status',
3 => 'subject',
4 => 'body',
3 => 'language',
4 => 'html_template',
5 => 'subject',
6 => 'body',
// 5 => 'importance', not handled when sending the mail, better hide it then
),
'fieldset:ActionEmail:trigger' => array(
@@ -281,20 +303,21 @@ class ActionEmail extends ActionNotification
2 => 'reply_to',
3 => 'reply_to_label',
4 => 'test_recipient',
5 => 'to',
6 => 'cc',
7 => 'bcc',
5 => 'ignore_notify',
6 => 'to',
7 => 'cc',
8 => 'bcc',
),
),
));
// - Attributes to be displayed for a list
MetaModel::Init_SetZListItems('list', array('status', 'to', 'subject'));
MetaModel::Init_SetZListItems('list', array('status', 'to', 'subject', 'language'));
// Search criteria
// - Standard criteria of the search
MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'status', 'subject'));
MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'status', 'subject', 'language'));
// - Default criteria for the search
MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status', 'subject'));
MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status', 'subject', 'language'));
}
// count the recipients found
@@ -324,6 +347,15 @@ class ActionEmail extends ActionNotification
try
{
$oSearch = DBObjectSearch::FromOQL($sOQL);
if ($this->Get('ignore_notify') === 'no') {
// In theory it is possible to notify *any* kind of object,
// as long as there is an email attribute in the class
// So let's not assume that the selected class is a Person
$sFirstSelectedClass = $oSearch->GetClass();
if (MetaModel::IsValidAttCode($sFirstSelectedClass, 'notify')) {
$oSearch->AddCondition('notify', 'yes');
}
}
$oSearch->AllowAllData();
}
catch (OQLException $e)
@@ -448,114 +480,27 @@ class ActionEmail extends ActionNotification
*/
protected function _DoExecute($oTrigger, $aContextArgs, &$oLog)
{
$sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass();
try
{
$this->m_iRecipients = 0;
$this->m_aMailErrors = array();
// Determine recipients
//
$sTo = $this->FindRecipients('to', $aContextArgs);
$sCC = $this->FindRecipients('cc', $aContextArgs);
$sBCC = $this->FindRecipients('bcc', $aContextArgs);
$sFrom = MetaModel::ApplyParams($this->Get('from'), $aContextArgs);
$sFromLabel = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs);
$sReplyTo = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs);
$sReplyToLabel = MetaModel::ApplyParams($this->Get('reply_to_label'), $aContextArgs);
$sSubject = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs);
$sBody = MetaModel::ApplyParams($this->Get('body'), $aContextArgs);
$oObj = $aContextArgs['this->object()'];
$sMessageId = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_MESSAGE_ID);
$sReference = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_REFERENCES);
}
catch (Exception $e) {
/** @noinspection PhpUnhandledExceptionInspection */
throw $e;
}
finally {
ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker);
}
if (!is_null($oLog)) {
// Note: we have to secure this because those values are calculated
// inside the try statement, and we would like to keep track of as
// many data as we could while some variables may still be undefined
if (isset($sTo)) {
$oLog->Set('to', $sTo);
}
if (isset($sCC)) {
$oLog->Set('cc', $sCC);
}
if (isset($sBCC)) {
$oLog->Set('bcc', $sBCC);
}
if (isset($sFrom)) {
$oLog->Set('from', $sFrom);
}
if (isset($sSubject)) {
$oLog->Set('subject', $sSubject);
}
if (isset($sBody)) {
$oLog->Set('body', $sBody);
}
}
$sStyles = file_get_contents(APPROOT.'css/email.css');
$sStyles .= MetaModel::GetConfig()->Get('email_css');
$oEmail = new EMail();
if ($this->IsBeingTested()) {
$oEmail->SetSubject('TEST['.$sSubject.']');
$sTestBody = $sBody;
$sTestBody .= "<div style=\"border: dashed;\">\n";
$sTestBody .= "<h1>Testing email notification ".$this->GetHyperlink()."</h1>\n";
$sTestBody .= "<p>The email should be sent with the following properties\n";
$sTestBody .= "<ul>\n";
$sTestBody .= "<li>TO: $sTo</li>\n";
$sTestBody .= "<li>CC: $sCC</li>\n";
$sTestBody .= "<li>BCC: $sBCC</li>\n";
$sTestBody .= empty($sFromLabel) ? "<li>From: $sFrom</li>\n" : "<li>From: $sFromLabel &lt;$sFrom&gt;</li>\n";
$sTestBody .= empty($sReplyToLabel) ? "<li>Reply-To: $sReplyTo</li>\n" : "<li>Reply-To: $sReplyToLabel &lt;$sReplyTo&gt;</li>\n";
$sTestBody .= "<li>References: $sReference</li>\n";
$sTestBody .= "</ul>\n";
$sTestBody .= "</p>\n";
$sTestBody .= "</div>\n";
$oEmail->SetBody($sTestBody, 'text/html', $sStyles);
$oEmail->SetRecipientTO($this->Get('test_recipient'));
$oEmail->SetRecipientFrom($sFrom, $sFromLabel);
$oEmail->SetReferences($sReference);
$oEmail->SetMessageId($sMessageId);
// Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
$oEmail->SetInReplyTo($sReference);
} else {
$oEmail->SetSubject($sSubject);
$oEmail->SetBody($sBody, 'text/html', $sStyles);
$oEmail->SetRecipientTO($sTo);
$oEmail->SetRecipientCC($sCC);
$oEmail->SetRecipientBCC($sBCC);
$oEmail->SetRecipientFrom($sFrom, $sFromLabel);
$oEmail->SetRecipientReplyTo($sReplyTo, $sReplyToLabel);
$oEmail->SetReferences($sReference);
$oEmail->SetMessageId($sMessageId);
// Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
$oEmail->SetInReplyTo($sReference);
$aEmailContent = $this->PrepareMessageContent($aContextArgs, $oLog);
$oEmail->SetSubject($aEmailContent['subject']);
$oEmail->SetBody($aEmailContent['body'], 'text/html', $sStyles);
$oEmail->SetRecipientTO($aEmailContent['to']);
$oEmail->SetRecipientCC($aEmailContent['cc']);
$oEmail->SetRecipientBCC($aEmailContent['bcc']);
$oEmail->SetRecipientFrom($aEmailContent['from'], $aEmailContent['from_label']);
$oEmail->SetRecipientReplyTo($aEmailContent['reply_to'], $aEmailContent['reply_to_label']);
$oEmail->SetReferences($aEmailContent['references']);
$oEmail->SetMessageId($aEmailContent['message_id']);
$oEmail->SetInReplyTo($aEmailContent['in_reply_to']);
foreach($aEmailContent['attachments'] as $aAttachment) {
$oEmail->AddAttachment($aAttachment['data'], $aAttachment['filename'], $aAttachment['mime_type']);
}
if (isset($aContextArgs['attachments']))
{
$aAttachmentReport = array();
foreach($aContextArgs['attachments'] as $oDocument)
{
$oEmail->AddAttachment($oDocument->GetData(), $oDocument->GetFileName(), $oDocument->GetMimeType());
$aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData()));
}
$oLog->Set('attachments', $aAttachmentReport);
}
if (empty($this->m_aMailErrors))
{
if ($this->m_iRecipients == 0)
@@ -564,6 +509,7 @@ class ActionEmail extends ActionNotification
}
else
{
$aErrors = [];
$iRes = $oEmail->Send($aErrors, false, $oLog); // allow asynchronous mode
switch ($iRes)
{
@@ -588,13 +534,147 @@ class ActionEmail extends ActionNotification
}
}
/**
* @param array $aContextArgs
* @param \EventNotification $oLog
*
* @return array
* @throws \CoreException
* @throws \Exception
* @since 3.1.0 N°918
*/
protected function PrepareMessageContent($aContextArgs, &$oLog): array
{
$aMessageContent = [
'to' => '',
'cc' => '',
'bcc' => '',
'from' => '',
'from_label' => '',
'reply_to' => '',
'reply_to_label' => '',
'subject' => '',
'body' => '',
'references' => '',
'message_id' => '',
'in_reply_to' => '',
'attachments' => [],
];
$sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass();
$sPreviousLanguage = Dict::GetUserLanguage();
$aPreviousPluginProperties = ApplicationContext::GetPluginProperties('QueryLocalizerPlugin');
if ($this->Get('language') !== '') {
// If a language is specified for this action, force this language
// when rendering all placeholders inside this message
Dict::SetUserLanguage($this->Get('language'));
AttributeDateTime::LoadFormatFromConfig();
ApplicationContext::SetPluginProperty('QueryLocalizerPlugin', 'language_code', $this->Get('language'));
}
try
{
$this->m_iRecipients = 0;
$this->m_aMailErrors = array();
// Determine recipients
//
$aMessageContent['to'] = $this->FindRecipients('to', $aContextArgs);
$aMessageContent['cc'] = $this->FindRecipients('cc', $aContextArgs);
$aMessageContent['bcc'] = $this->FindRecipients('bcc', $aContextArgs);
$aMessageContent['from'] = MetaModel::ApplyParams($this->Get('from'), $aContextArgs);
$aMessageContent['from_label'] = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs);
$aMessageContent['reply_to'] = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs);
$aMessageContent['reply_to_label'] = MetaModel::ApplyParams($this->Get('reply_to_label'), $aContextArgs);
$aMessageContent['subject'] = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs);
$sBody = $this->BuildMessageBody(false);
$aMessageContent['body'] = MetaModel::ApplyParams($sBody, $aContextArgs);
$oObj = $aContextArgs['this->object()'];
$aMessageContent['message_id'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_MESSAGE_ID);
$aMessageContent['references'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_REFERENCES);
}
catch (Exception $e) {
/** @noinspection PhpUnhandledExceptionInspection */
throw $e;
}
finally {
ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker);
Dict::SetUserLanguage($sPreviousLanguage);
AttributeDateTime::LoadFormatFromConfig();
ApplicationContext::SetPluginProperty('QueryLocalizerPlugin', 'language_code', $aPreviousPluginProperties['language_code'] ?? null);
}
if (!is_null($oLog)) {
// Note: we have to secure this because those values are calculated
// inside the try statement, and we would like to keep track of as
// many data as we could while some variables may still be undefined
if (isset($aMessageContent['to'])) {
$oLog->Set('to', $aMessageContent['to']);
}
if (isset($aMessageContent['cc'])) {
$oLog->Set('cc', $aMessageContent['cc']);
}
if (isset($aMessageContent['bcc'])) {
$oLog->Set('bcc', $aMessageContent['bcc']);
}
if (isset($aMessageContent['from'])) {
$oLog->Set('from', $aMessageContent['from']);
}
if (isset($aMessageContent['subject'])) {
$oLog->Set('subject', $aMessageContent['subject']);
}
if (isset($aMessageContent['body'])) {
$oLog->Set('body', HTMLSanitizer::Sanitize($aMessageContent['body']));
}
}
$sStyles = file_get_contents(APPROOT.'css/email.css');
$sStyles .= MetaModel::GetConfig()->Get('email_css');
if ($this->IsBeingTested()) {
$sTestBody = $aMessageContent['body'];
$sTestBody .= "<div style=\"border: dashed;\">\n";
$sTestBody .= "<h1>Testing email notification ".$this->GetHyperlink()."</h1>\n";
$sTestBody .= "<p>The email should be sent with the following properties\n";
$sTestBody .= "<ul>\n";
$sTestBody .= "<li>TO: {$aMessageContent['to']}</li>\n";
$sTestBody .= "<li>CC: {$aMessageContent['cc']}</li>\n";
$sTestBody .= "<li>BCC: {$aMessageContent['bcc']}</li>\n";
$sTestBody .= empty($aMessageContent['from_label']) ? "<li>From: {$aMessageContent['from']}</li>\n" : "<li>From: {$aMessageContent['from_label']} &lt;{$aMessageContent['from']}&gt;</li>\n";
$sTestBody .= empty($aMessageContent['reply_to_label']) ? "<li>Reply-To: {$aMessageContent['reply_to']}</li>\n" : "<li>Reply-To: {$aMessageContent['reply_to_label']} &lt;{$aMessageContent['reply_to']}&gt;</li>\n";
$sTestBody .= "<li>References: {$aMessageContent['references']}</li>\n";
$sTestBody .= "</ul>\n";
$sTestBody .= "</p>\n";
$sTestBody .= "</div>\n";
$aMessageContent['subject'] = 'TEST['.$aMessageContent['subject'].']';
$aMessageContent['body'] = $sTestBody;
$aMessageContent['to'] = $this->Get('test_recipient');
}
// Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
$aMessageContent['in_reply_to'] = $aMessageContent['references'];
if (isset($aContextArgs['attachments']))
{
$aAttachmentReport = array();
foreach($aContextArgs['attachments'] as $oDocument)
{
$aMessageContent['attachments'][] = ['data' => $oDocument->GetData(), 'filename' => $oDocument->GetFileName(), 'mime_type' => $oDocument->GetMimeType()];
$aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData()));
}
$oLog->Set('attachments', $aAttachmentReport);
}
return $aMessageContent;
}
/**
* @param \DBObject $oObject
* @param string $sHeaderName {@see \ActionEmail::ENUM_HEADER_NAME_REFERENCES}, {@see \ActionEmail::ENUM_HEADER_NAME_MESSAGE_ID}
*
* @return string The formatted identifier for $sHeaderName based on $oObject
* @throws \Exception
* @since 3.0.1 N°4849
* @since 3.1.0 N°4849
*/
protected function GenerateIdentifierForHeaders(DBObject $oObject, string $sHeaderName): string
{
@@ -629,4 +709,65 @@ class ActionEmail extends ActionNotification
]);
throw new Exception($sErrorMessage);
}
/**
* Compose the body of the message from the 'body' attribute and the HTML template (if any)
* @since 3.1.0 N°4849
* @param bool $bHighlightPlaceholders If true add some extra HTML around placeholders to highlight them
* @return string
*/
protected function BuildMessageBody(bool $bHighlightPlaceholders = false): string
{
$sBody = $this->Get('body');
/** @var ormDocument $oHtmlTemplate */
$oHtmlTemplate = $this->Get('html_template');
if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) {
$sHtmlTemplate = $oHtmlTemplate->GetData();
if (false !== mb_strpos($sHtmlTemplate, static::TEMPLATE_BODY_CONTENT)) {
if ($bHighlightPlaceholders) {
// Highlight the $content$ block
$sBody = sprintf(static::CONTENT_HIGHLIGHT, $sBody);
}
$sBody = str_replace(static::TEMPLATE_BODY_CONTENT, $sBody, $oHtmlTemplate->GetData()); // str_replace is ok as long as both strings are well-formed UTF-8
} else {
$sBody = $oHtmlTemplate->GetData();
}
}
if($bHighlightPlaceholders) {
// Highlight all placeholders
$sBody = preg_replace('/\\$([^$]+)\\$/', static::FIELD_HIGHLIGHT, $sBody);
}
return $sBody;
}
/**
* @since 3.1.0 N°4849
* @inheritDoc
* @see cmdbAbstractObject::DisplayBareRelations()
*/
public function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
{
parent::DisplayBareRelations($oPage, false);
if (!$bEditMode) {
$oPage->SetCurrentTab('action_email__preview', Dict::S('ActionEmail:preview_tab'), Dict::S('ActionEmail:preview_tab+'));
$sBody = $this->BuildMessageBody(true);
TwigHelper::RenderIntoPage($oPage, APPROOT.'/', 'templates/datamodel/ActionEmail/email-notification-preview', ['iframe_content' => $sBody]);
}
}
/**
* @since 3.1.0
* @inheritDoc
* @see cmdbAbstractObject::DoCheckToWrite()
*/
public function DoCheckToWrite()
{
parent::DoCheckToWrite();
$oHtmlTemplate = $this->Get('html_template');
if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) {
if (false === mb_strpos($oHtmlTemplate->GetData(), static::TEMPLATE_BODY_CONTENT)) {
$this->m_aCheckWarnings[] = Dict::Format('ActionEmail:content_placeholder_missing', static::TEMPLATE_BODY_CONTENT, Dict::S('Class:ActionEmail/Attribute:body'));
}
}
}
}

View File

@@ -6214,7 +6214,7 @@ class AttributeDateTime extends AttributeDBField
/**
* Load the 3 settings: date format, time format and data_time format from the configuration
*/
protected static function LoadFormatFromConfig()
public static function LoadFormatFromConfig()
{
$aFormats = MetaModel::GetConfig()->Get('date_and_time_format');
$sLang = Dict::GetUserLanguage();
@@ -8343,7 +8343,7 @@ class AttributeBlob extends AttributeDefinition
$aValues[$this->GetCode().'_data'] = '';
$aValues[$this->GetCode().'_mimetype'] = '';
$aValues[$this->GetCode().'_filename'] = '';
$aValues[$this->GetCode().'_downloads_count'] = ''; // Note: Should this be set to \ormDocument::DEFAULT_DOWNLOADS_COUNT ?
$aValues[$this->GetCode().'_downloads_count'] = \ormDocument::DEFAULT_DOWNLOADS_COUNT;
}
return $aValues;

View File

@@ -569,9 +569,22 @@ While editing, click on the magnifier to get pertinent examples',
'Class:ActionEmail/Attribute:importance/Value:normal+' => '',
'Class:ActionEmail/Attribute:importance/Value:high' => 'High',
'Class:ActionEmail/Attribute:importance/Value:high+' => '',
'Class:ActionEmail/Attribute:language' => 'Language',
'Class:ActionEmail/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)',
'Class:ActionEmail/Attribute:html_template' => 'HTML template',
'Class:ActionEmail/Attribute:html_template+' => 'Optional HTML template wrapping around the content of the \'Body\' attribute below, useful for tailored email layouts (in the template, content of the \'Body\' attribute will replace the $content$ placeholder)',
'Class:ActionEmail/Attribute:ignore_notify' => 'Ignore the Notify flag',
'Class:ActionEmail/Attribute:ignore_notify+' => 'If set to \'Yes\' the \'Notify\' flag on Contacts has no effect.',
'Class:ActionEmail/Attribute:ignore_notify/Value:no' => 'No',
'Class:ActionEmail/Attribute:ignore_notify/value:yes' => 'Yes',
'ActionEmail:main' => 'Message',
'ActionEmail:trigger' => 'Triggers',
'ActionEmail:recipients' => 'Contacts',
'ActionEmail:preview_tab' => 'Preview',
'ActionEmail:preview_tab+' => 'Preview of the eMail template',
'ActionEmail:preview_warning' => 'The actual eMail may look different in the eMail client than this preview in your browser.',
'ActionEmail:preview_more_info' => 'For more information about the CSS features supported by the different eMail clients, refer to %1$s',
'ActionEmail:content_placeholder_missing' => 'The placeholder "%1$s" was not found in the HTML template. The content of the field "%2$s" will not be included in the generated emails.',
));
//

View File

@@ -569,10 +569,22 @@ En édition, cliquez sur la loupe pour obtenir des exemples pertinents.',
'Class:ActionEmail/Attribute:importance/Value:normal+' => '',
'Class:ActionEmail/Attribute:importance/Value:high' => 'Haute',
'Class:ActionEmail/Attribute:importance/Value:high+' => '',
'Class:ActionEmail/Attribute:language' => 'Langue',
'Class:ActionEmail/Attribute:language+' => 'Langue utilisée pour les placeholders ($xxx$) dans le message (statut, importance, priorité, etc)',
'Class:ActionEmail/Attribute:html_template' => 'Modèle HTML',
'Class:ActionEmail/Attribute:html_template+' => 'Optionnel, modèle HTML encapsulant le contenu du champ \'Message\' ci-dessous, utile pour des mises en page sur mesure (dans le modèle, le contenu du champ \'Message\' remplacera le mot-clé $content$)',
'Class:ActionEmail/Attribute:ignore_notify' => 'Ignorer la préférence \'Notification\'',
'Class:ActionEmail/Attribute:ignore_notify+' => 'Si \'Oui\', le champ \'Notification\' des Contacts est ignoré.',
'Class:ActionEmail/Attribute:ignore_notify/Value:no' => 'Non',
'Class:ActionEmail/Attribute:ignore_notify/Value:yes' => 'Oui',
'ActionEmail:main' => 'Message',
'ActionEmail:trigger' => 'Conditions de déclenchement',
'ActionEmail:recipients' => 'Contacts',
'ActionEmail:preview_tab' => 'Aperçu',
'ActionEmail:preview_tab+' => 'Aperçu du modèle de mèl',
'ActionEmail:preview_warning' => 'Le mèl peut s\'afficher différement dans les clients mèl par rapport à cet aperçu dans votre navigateur.',
'ActionEmail:preview_more_info' => 'Pour plus d\'informations sur les fonctionnalités CSS supportées par les différents client mèl, consultez %1$s.',
'ActionEmail:content_placeholder_missing' => 'The mot-clé "%1$s" ne figure pas dans le modèle HTML. Le contenu du champ "%2$s" ne sera pas intégré dans les mèls générés.',
));
//

View File

@@ -0,0 +1,13 @@
{% apply spaceless %}
{% UIAlert ForWarning{sTitle:'', sContent: '', sId: null} %}
<div id="branding-error-alert-content">
<div style="display:flex;flex-align: stretch;">
<div style="margin-right:1em"><span class="fas fa-2x fa-exclamation-triangle"></span></div>
<div>{{ 'ActionEmail:preview_warning'|dict_s }}<br/>{{ 'ActionEmail:preview_more_info'|dict_format('<a href="https://www.caniemail.com" target="_blank">www.canimeail.com</a>')|raw }}</div>
</div>
</div>
{% EndUIAlert %}
<div style="display:flex;align-items:stretch;height:50rem;margin-top:0.5rem;">
<iframe width="100%" sandbox srcdoc="{{ iframe_content }}"></iframe>
</div>
{% endapply %}

View File

@@ -7,6 +7,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use MetaModel;
use utils;
use Dict;
/**
* @runTestsInSeparateProcesses
@@ -23,11 +24,22 @@ class ActionEmailTest extends ItopDataTestCase
/** @var \ActionEmail|null Temp ActionEmail created for tests */
protected static $oActionEmail = null;
/** @var \ormDocument|null Temp ormDocument for tests */
protected static $oDocument = null;
/** @var \UserRequest|null Temp ormDocument for tests */
protected static $oUserRequest = null;
/** @var string[] Dict formatted message, because the Dict class is not available in providers */
protected static $aWarningMessages;
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('application/Html2Text.php');
static::$oActionEmail = MetaModel::NewObject('ActionEmail', [
'name' => 'Test action',
'status' => 'disabled',
@@ -35,7 +47,27 @@ class ActionEmailTest extends ItopDataTestCase
'subject' => 'Test subject',
'body' => 'Test body',
]);
$sHtml =
<<<HTML
<body>
<table data-something-that-would-be-removed-by-the-sanitizer-through-ckeditor-but-that-will-stay-with-the-template="bar">
<tr><td>Formatted eMail</td></tr>
<tr><td>\$content\$</td></tr>
</body>
HTML
;
static::$oActionEmail->DBInsert();
static::$oDocument = new \ormDocument($sHtml, 'text/html', 'sample.html');
static::$oUserRequest = MetaModel::NewObject('UserRequest', [
'title' => 'Test UserRequest',
'description' => '<p>Multi-line<br/>description</p>'
]);
static::$aWarningMessages = [
'warning-missing-content' => Dict::Format('ActionEmail:content_placeholder_missing', '$content$', Dict::S('Class:ActionEmail/Attribute:body'))
];
}
/**
@@ -77,7 +109,6 @@ class ActionEmailTest extends ItopDataTestCase
$this->assertEquals($sExpectedIdentifier, $sTestedIdentifier);
break;
}
}
public function GenerateIdentifierForHeadersProvider()
@@ -88,4 +119,206 @@ class ActionEmailTest extends ItopDataTestCase
'IncorrectHeaderName' => ['IncorrectHeaderName'],
];
}
/**
* @dataProvider prepareMessageContentProvider
*/
public function testPrepareMessageContent($sCurrentUserLanguage, $aActionFields, $aFieldsToCheck)
{
\Dict::SetUserLanguage($sCurrentUserLanguage);
$aContext = ['this->object()' => static::$oUserRequest];
$oActionEmail = MetaModel::NewObject('ActionEmail', [
'name' => 'Test action',
'status' => 'disabled',
'from' => 'unit-test@openitop.org',
'subject' => 'Test subject',
'body' => 'Test body',
]);
foreach($aActionFields as $sCode => $value) {
if ($sCode === 'html_template') {
// special case since the data provider cannot create ormDocument objects
$oActionEmail->Set($sCode, static::$oDocument);
} else {
$oActionEmail->Set($sCode, $value);
}
}
$oActionEmail->DBInsert();
$oOrg = $this->CreateOrganization('testPrepareMessageContent');
$oContact1 = MetaModel::NewObject('Person', [
'name' => 'Person 1',
'first_name' => 'PrepareMessageContent',
'org_id' => $oOrg->GetKey(),
'email' => 'some.valid@email.com',
'notify' => 'yes',
]);
$oContact1->DBInsert();
$oContact2 = MetaModel::NewObject('Person', [
'name' => 'Person 2',
'first_name' => 'PrepareMessageContent',
'org_id' => $oOrg->GetKey(),
'email' => 'some.valid2@email.com',
'notify' => 'no',
]);
$oContact2->DBInsert();
$oLog = null;
$aEmailContent = $this->InvokeNonPublicMethod('\ActionEmail', 'PrepareMessageContent', $oActionEmail, [$aContext, &$oLog]);
// Normalize the content of the body to simplify the comparison, useful when status == test
$aEmailContent['body'] = preg_replace('/title="[^"]+"/', 'title="****"', $aEmailContent['body']);
$aEmailContent['body'] = preg_replace('/class="object-ref-link" href="[^"]+"/', 'class="object-ref-link" href="****"', $aEmailContent['body']);
$aEmailContent['body'] = preg_replace('/References: <[^>]+>/', 'References: ****', $aEmailContent['body']);
foreach($aFieldsToCheck as $sCode => $expectedValue) {
$this->assertEquals($expectedValue, $aEmailContent[$sCode]);
}
}
public function prepareMessageContentProvider()
{
return [
'subject-no-placeholder' => [
'EN US',
['subject' => 'This is a test'],
['subject' => 'This is a test'],
],
'subject-with-placeholder' => [
'EN US',
['subject' => 'Ticket "$this->title$" created'],
['subject' => 'Ticket "Test UserRequest" created'],
],
'simple-to-oql' => [
'EN US',
['to' => "SELECT Person WHERE email='some.valid@email.com'"],
['to' => 'some.valid@email.com'],
],
'simple-to-oql_ignoring_ignore_notify' => [
'EN US',
['to' => "SELECT Person WHERE email='some.valid2@email.com'"],
['to' => 'some.valid2@email.com'], // contact2 has 'notify' set to 'no' BUT by default when don't care
],
'simple-to-oql-not-bypassing-notify' => [
'EN US',
['to' => "SELECT Person WHERE email='some.valid2@email.com'", 'ignore_notify' => 'no'],
['to' => ''], // contact2 has 'notify' set to 'no'
],
'subject-with-localized-placeholder (default behavior)' => [
'EN US',
['subject' => 'Ticket in state "$this->label(status)$" created'],
['subject' => 'Ticket in state "New" created'],
],
'subject-with-localized-placeholder (default behavior 2)' => [
'FR FR',
['subject' => 'Ticket in state "$this->label(status)$" created'],
['subject' => 'Ticket in state "Nouveau" created'],
],
'subject-with-localized-placeholder (new behavior)' => [
'FR FR',
['subject' => 'Ticket in state "$this->label(status)$" created', 'language' => 'EN US'],
['subject' => 'Ticket in state "New" created'],
],
'simple-body-with-placeholder' => [
'EN US',
['body' => '<p>Ticket "$this->title$" created.</p>'],
['body' => '<p>Ticket "Test UserRequest" created.</p>'],
],
'simple-body-with-placeholder-TEST-mode' => [
'EN US',
['body' => '<p>Ticket "$this->title$" created.</p>', 'status' => 'test'],
['body' =>
<<<HTML
<p>Ticket "Test UserRequest" created.</p><div style="border: dashed;">
<h1>Testing email notification <span class="object-ref " title="****"><a class="object-ref-link" href="****">Test action</a></span></h1>
<p>The email should be sent with the following properties
<ul>
<li>TO: </li>
<li>CC: </li>
<li>BCC: </li>
<li>From: unit-test@openitop.org</li>
<li>Reply-To: </li>
<li>References: ****</li>
</ul>
</p>
</div>
HTML
,
'subject' => 'TEST[Test subject]',
],
],
'more-complex-body-and-title-with-placeholder' => [
'EN US',
['subject' => 'Ticket "$this->title$" created'],
['subject' => 'Ticket "Test UserRequest" created'],
['body' => '<h1>Ticket "$this->title$" created.</h1><p>Description: $this->html(description)</p>'],
['body' => '<h1>Ticket "Test UserRequest" created.</h1><p>Description: <p>Multi-line<br/>description</p></p>'],
],
'simple-body-with-placeholder_and_template' => [
'EN US',
['body' => '<p>Ticket "$this->title$" created.</p>', 'html_template' => true],
['body' =>
<<<HTML
<body>
<table data-something-that-would-be-removed-by-the-sanitizer-through-ckeditor-but-that-will-stay-with-the-template="bar">
<tr><td>Formatted eMail</td></tr>
<tr><td><p>Ticket "Test UserRequest" created.</p></td></tr>
</body>
HTML
],
],
];
}
/**
* @dataProvider doCheckToWriteProvider
* @param string $sBody
* @param string $sHtmlTemplate
* @param string[] $aExpectedWarnings
*/
public function testDoCheckToWrite(string $sBody, ?string $sHtmlTemplate, $expectedWarnings)
{
$oActionEmail = new ActionEmail();
// Set mandatory fields
$oActionEmail->Set('name', 'test');
$oActionEmail->Set('subject', 'Ga Bu Zo Meu');
// Set the fields for testing
$oActionEmail->Set('body', $sBody);
if ($sHtmlTemplate !== null) {
$oDoc = new \ormDocument($sHtmlTemplate, 'text/html', 'template.html');
$oActionEmail->Set('html_template', $oDoc);
}
$oActionEmail->DoCheckToWrite();
$aWarnings = $this->GetNonPublicProperty($oActionEmail, 'm_aCheckWarnings');
if ($expectedWarnings === null) {
$this->assertEquals($aWarnings, $expectedWarnings);
} else {
// The warning messages are localized, but the provider functions does not
// have access to the Dict class, so let's replace the value given by the
// provider by a statically precomputed and localized message
foreach($expectedWarnings as $index => $sMessageKey) {
$expectedWarnings[$index] = static::$aWarningMessages[$sMessageKey];
}
$this->assertEquals($aWarnings, $expectedWarnings);
}
}
public function doCheckToWriteProvider()
{
return [
'no warnings' => [
'<p>Some text here</p>',
'<div>$content$</div>',
null
],
'$content$ missing' => [
'<p>Some text here</p>',
'<div>no placeholder</div>',
[ 'warning-missing-content' ]
],
];
}
}