mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
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:
@@ -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 <$sFrom></li>\n";
|
||||
$sTestBody .= empty($sReplyToLabel) ? "<li>Reply-To: $sReplyTo</li>\n" : "<li>Reply-To: $sReplyToLabel <$sReplyTo></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']} <{$aMessageContent['from']}></li>\n";
|
||||
$sTestBody .= empty($aMessageContent['reply_to_label']) ? "<li>Reply-To: {$aMessageContent['reply_to']}</li>\n" : "<li>Reply-To: {$aMessageContent['reply_to_label']} <{$aMessageContent['reply_to']}></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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -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.',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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' ]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user