From 6ca443a2556e5d4bade9a9ef7d6d877bf1d6fc45 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 5 Sep 2017 17:39:45 -0400 Subject: PLT-7522 Cleaned up translation of templates (#7351) * PLT-7522 Cleaned up translation of templates * Added unit tests * Changed TranslateAsHtml to not be variadic --- app/email.go | 35 +++++++++++++++++------------------ app/notification.go | 12 ++++++------ utils/html.go | 25 +++++++++++++++++++++++-- utils/html_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 utils/html_test.go diff --git a/app/email.go b/app/email.go index 377a586ea..bf9043de5 100644 --- a/app/email.go +++ b/app/email.go @@ -5,7 +5,6 @@ package app import ( "fmt" - "html/template" "net/url" l4g "github.com/alecthomas/log4go" @@ -24,8 +23,8 @@ func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL st bodyPage := utils.NewHTMLTemplate("email_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.username_change_body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.username_change_body.info", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.username_change_body.info", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername}) if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendChangeUsernameEmail", "api.user.send_email_change_username_and_forget.error", nil, err.Error(), http.StatusInternalServerError) @@ -68,8 +67,8 @@ func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.App bodyPage := utils.NewHTMLTemplate("email_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.email_change_body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.email_change_body.info", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.email_change_body.info", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail}) if err := utils.SendMail(oldEmail, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendEmailChangeEmail", "api.user.send_email_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) @@ -111,8 +110,8 @@ func SendSignInChangeEmail(email, method, locale, siteURL string) *model.AppErro bodyPage := utils.NewHTMLTemplate("signin_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.signin_change_email.body.info", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.signin_change_email.body.info", + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method}) if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) @@ -170,8 +169,8 @@ func SendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppEr bodyPage := utils.NewHTMLTemplate("password_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.password_change_body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.password_change_body.info", - map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.password_change_body.info", + map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method}) if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendPasswordChangeEmail", "api.user.send_password_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) @@ -188,8 +187,8 @@ func SendUserAccessTokenAddedEmail(email, locale string) *model.AppError { bodyPage := utils.NewHTMLTemplate("password_change_body", locale) bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.user_access_token_body.info", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "SiteURL": utils.GetSiteURL()})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.user_access_token_body.info", + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "SiteURL": utils.GetSiteURL()}) if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError) @@ -210,7 +209,7 @@ func SendPasswordResetEmail(email string, token *model.Token, locale, siteURL st bodyPage := utils.NewHTMLTemplate("reset_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.reset_body.title") - bodyPage.Html["Info"] = template.HTML(T("api.templates.reset_body.info")) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.reset_body.info", nil) bodyPage.Props["ResetUrl"] = link bodyPage.Props["Button"] = T("api.templates.reset_body.button") @@ -239,8 +238,7 @@ func SendMfaChangeEmail(email string, activated bool, locale, siteURL string) *m bodyPage.Props["Title"] = T("api.templates.mfa_deactivated_body.title") } - bodyPage.Html["Info"] = template.HTML(T(bodyText, - map[string]interface{}{"SiteURL": siteURL})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(T, bodyText, map[string]interface{}{"SiteURL": siteURL}) if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendMfaChangeEmail", "api.user.send_mfa_change_email.error", nil, err.Error(), http.StatusInternalServerError) @@ -262,11 +260,12 @@ func SendInviteEmails(team *model.Team, senderName string, invites []string, sit bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title") - bodyPage.Html["Info"] = template.HTML(utils.T("api.templates.invite_body.info", - map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName})) + bodyPage.Html["Info"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body.info", + map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName}) + bodyPage.Props["Info"] = map[string]interface{}{} bodyPage.Props["Button"] = utils.T("api.templates.invite_body.button") - bodyPage.Html["ExtraInfo"] = template.HTML(utils.T("api.templates.invite_body.extra_info", - map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name})) + bodyPage.Html["ExtraInfo"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body.extra_info", + map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name}) props := make(map[string]string) props["email"] = invite diff --git a/app/notification.go b/app/notification.go index c86e1669f..83a810026 100644 --- a/app/notification.go +++ b/app/notification.go @@ -427,11 +427,11 @@ func getNotificationEmailBody(recipient *model.User, post *model.Post, channel * t := getFormattedPostTime(post, translateFunc) var bodyText string - var info string + var info template.HTML if channel.Type == model.CHANNEL_DIRECT { if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { bodyText = translateFunc("app.notification.body.intro.direct.full") - info = translateFunc("app.notification.body.text.direct.full", + info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.full", map[string]interface{}{ "SenderName": senderName, "Hour": t.Hour, @@ -444,7 +444,7 @@ func getNotificationEmailBody(recipient *model.User, post *model.Post, channel * bodyText = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ "SenderName": senderName, }) - info = translateFunc("app.notification.body.text.direct.generic", + info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.generic", map[string]interface{}{ "Hour": t.Hour, "Minute": t.Minute, @@ -456,7 +456,7 @@ func getNotificationEmailBody(recipient *model.User, post *model.Post, channel * } else { if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { bodyText = translateFunc("app.notification.body.intro.notification.full") - info = translateFunc("app.notification.body.text.notification.full", + info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.full", map[string]interface{}{ "ChannelName": channelName, "SenderName": senderName, @@ -470,7 +470,7 @@ func getNotificationEmailBody(recipient *model.User, post *model.Post, channel * bodyText = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ "SenderName": senderName, }) - info = translateFunc("app.notification.body.text.notification.generic", + info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.generic", map[string]interface{}{ "Hour": t.Hour, "Minute": t.Minute, @@ -482,7 +482,7 @@ func getNotificationEmailBody(recipient *model.User, post *model.Post, channel * } bodyPage.Props["BodyText"] = bodyText - bodyPage.Html["Info"] = template.HTML(info) + bodyPage.Html["Info"] = info bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") return bodyPage.Render() diff --git a/utils/html.go b/utils/html.go index e6050f62a..dfbbe832d 100644 --- a/utils/html.go +++ b/utils/html.go @@ -93,8 +93,8 @@ func (t *HTMLTemplate) addDefaultProps() { t.Props["Organization"] = "" } - t.Html["EmailInfo"] = template.HTML(localT("api.templates.email_info", - map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName})) + t.Html["EmailInfo"] = TranslateAsHtml(localT, "api.templates.email_info", + map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName}) } func (t *HTMLTemplate) Render() string { @@ -116,5 +116,26 @@ func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { l4g.Error(T("api.api.render.error"), t.TemplateName, err) return err } + return nil } + +func TranslateAsHtml(t i18n.TranslateFunc, translationID string, args map[string]interface{}) template.HTML { + return template.HTML(t(translationID, escapeForHtml(args))) +} + +func escapeForHtml(arg interface{}) interface{} { + switch typedArg := arg.(type) { + case string: + return template.HTMLEscapeString(typedArg) + case map[string]interface{}: + safeArg := make(map[string]interface{}, len(typedArg)) + for key, value := range typedArg { + safeArg[key] = escapeForHtml(value) + } + return safeArg + default: + l4g.Warn("Unable to escape value for HTML template %v", arg) + return "" + } +} diff --git a/utils/html_test.go b/utils/html_test.go new file mode 100644 index 000000000..8dc70242a --- /dev/null +++ b/utils/html_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "html/template" + "testing" +) + +func TestTranslateAsHtml(t *testing.T) { + TranslationsPreInit() + + translateFunc := TfuncWithFallback("en") + + expected := "To finish updating your email address for YOUR TEAM HERE, please click the link below to confirm this is the right address." + if actual := TranslateAsHtml(translateFunc, "api.templates.email_change_verify_body.info", + map[string]interface{}{"TeamDisplayName": "YOUR TEAM HERE"}); actual != template.HTML(expected) { + t.Fatalf("Incorrectly translated template, got %v, expected %v", actual, expected) + } + + expected = "To finish updating your email address for <b>YOUR TEAM HERE</b>, please click the link below to confirm this is the right address." + if actual := TranslateAsHtml(translateFunc, "api.templates.email_change_verify_body.info", + map[string]interface{}{"TeamDisplayName": "YOUR TEAM HERE"}); actual != template.HTML(expected) { + t.Fatalf("Incorrectly translated template, got %v, expected %v", actual, expected) + } +} + +func TestEscapeForHtml(t *testing.T) { + input := "abc" + expected := "abc" + if actual := escapeForHtml(input).(string); actual != expected { + t.Fatalf("incorrectly escaped %v, got %v expected %v", input, actual, expected) + } + + input = "abc" + expected = "<b>abc</b>" + if actual := escapeForHtml(input).(string); actual != expected { + t.Fatalf("incorrectly escaped %v, got %v expected %v", input, actual, expected) + } + + inputMap := map[string]interface{}{ + "abc": "abc", + "123": "123", + } + expectedMap := map[string]interface{}{ + "abc": "abc", + "123": "<b>123</b>", + } + if actualMap := escapeForHtml(inputMap).(map[string]interface{}); actualMap["abc"] != expectedMap["abc"] || actualMap["123"] != expectedMap["123"] { + t.Fatalf("incorrectly escaped %v, got %v expected %v", inputMap, actualMap, expectedMap) + } +} -- cgit v1.2.3-1-g7c22