diff options
-rw-r--r-- | api/api.go | 3 | ||||
-rw-r--r-- | api/user.go | 2 | ||||
-rw-r--r-- | api4/api.go | 2 | ||||
-rw-r--r-- | app/app.go | 18 | ||||
-rw-r--r-- | app/apptestlib.go | 1 | ||||
-rw-r--r-- | app/email.go | 54 | ||||
-rw-r--r-- | app/email_batching.go | 6 | ||||
-rw-r--r-- | app/notification.go | 4 | ||||
-rw-r--r-- | app/session.go | 2 | ||||
-rw-r--r-- | app/user.go | 4 | ||||
-rw-r--r-- | cmd/platform/user.go | 3 | ||||
-rw-r--r-- | utils/html.go | 114 | ||||
-rw-r--r-- | utils/html_test.go | 158 | ||||
-rw-r--r-- | web/web.go | 3 |
14 files changed, 245 insertions, 129 deletions
diff --git a/api/api.go b/api/api.go index 4f593cfa5..44651b1fa 100644 --- a/api/api.go +++ b/api/api.go @@ -10,7 +10,6 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" _ "github.com/nicksnyder/go-i18n/i18n" ) @@ -113,8 +112,6 @@ func Init(a *app.App, root *mux.Router) *API { // 404 on any api route before web.go has a chance to serve it root.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) - utils.InitHTML() - a.InitEmailBatching() if *a.Config().ServiceSettings.EnableAPIv3 { diff --git a/api/user.go b/api/user.go index f8cb1d855..5aba61e38 100644 --- a/api/user.go +++ b/api/user.go @@ -1040,7 +1040,7 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { return } - if err := app.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil { + if err := c.App.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil { l4g.Error(err.Error()) } }) diff --git a/api4/api.go b/api4/api.go index 3c1d68384..b9a24cafd 100644 --- a/api4/api.go +++ b/api4/api.go @@ -222,8 +222,6 @@ func Init(a *app.App, root *mux.Router, full bool) *API { // REMOVE CONDITION WHEN APIv3 REMOVED if full { - utils.InitHTML() - a.InitEmailBatching() } diff --git a/app/app.go b/app/app.go index 49ac620e8..55fb43b30 100644 --- a/app/app.go +++ b/app/app.go @@ -4,6 +4,7 @@ package app import ( + "html/template" "net/http" "runtime/debug" "sync/atomic" @@ -52,7 +53,8 @@ type App struct { configFile string newStore func() store.Store - sessionCache *utils.Cache + htmlTemplateWatcher *utils.HTMLTemplateWatcher + sessionCache *utils.Cache } var appCount = 0 @@ -94,6 +96,12 @@ func New(options ...Option) *App { } } + if htmlTemplateWatcher, err := utils.NewHTMLTemplateWatcher("templates"); err != nil { + l4g.Error(utils.T("api.api.init.parsing_templates.error"), err) + } else { + app.htmlTemplateWatcher = htmlTemplateWatcher + } + app.Srv.Store = app.newStore() app.initJobs() @@ -125,6 +133,10 @@ func (a *App) Shutdown() { a.Srv.Store.Close() a.Srv = nil + if a.htmlTemplateWatcher != nil { + a.htmlTemplateWatcher.Close() + } + l4g.Info(utils.T("api.server.stop_server.stopped.info")) } @@ -327,6 +339,10 @@ func (a *App) WaitForGoroutines() { } } +func (a *App) HTMLTemplates() *template.Template { + return a.htmlTemplateWatcher.Templates() +} + func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound) err.Translate(utils.T) diff --git a/app/apptestlib.go b/app/apptestlib.go index b34749be3..63a064d7f 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -66,7 +66,6 @@ func setupTestHelper(enterprise bool) *TestHelper { } th.App.StartServer() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) - utils.InitHTML() utils.EnableDebugLogForTest() th.App.Srv.Store.MarkSystemRanUnitTests() diff --git a/app/email.go b/app/email.go index b676685e2..00a32352a 100644 --- a/app/email.go +++ b/app/email.go @@ -10,6 +10,8 @@ import ( "net/http" l4g "github.com/alecthomas/log4go" + "github.com/nicksnyder/go-i18n/i18n" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -21,7 +23,7 @@ func (a *App) SendChangeUsernameEmail(oldUsername, newUsername, email, locale, s map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) - bodyPage := utils.NewHTMLTemplate("email_change_body", locale) + bodyPage := a.NewEmailTemplate("email_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.username_change_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.username_change_body.info", @@ -43,7 +45,7 @@ func (a *App) SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token st map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) - bodyPage := utils.NewHTMLTemplate("email_change_verify_body", locale) + bodyPage := a.NewEmailTemplate("email_change_verify_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.email_change_verify_body.title") bodyPage.Props["Info"] = T("api.templates.email_change_verify_body.info", @@ -65,7 +67,7 @@ func (a *App) SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) * map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) - bodyPage := utils.NewHTMLTemplate("email_change_body", locale) + bodyPage := a.NewEmailTemplate("email_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.email_change_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.email_change_body.info", @@ -88,7 +90,7 @@ func (a *App) SendVerifyEmail(userEmail, locale, siteURL, token string) *model.A subject := T("api.templates.verify_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("verify_body", locale) + bodyPage := a.NewEmailTemplate("verify_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.verify_body.title", map[string]interface{}{"ServerURL": url.Host}) bodyPage.Props["Info"] = T("api.templates.verify_body.info") @@ -108,7 +110,7 @@ func (a *App) SendSignInChangeEmail(email, method, locale, siteURL string) *mode subject := T("api.templates.signin_change_email.subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("signin_change_body", locale) + bodyPage := a.NewEmailTemplate("signin_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.signin_change_email.body.info", @@ -130,7 +132,7 @@ func (a *App) SendWelcomeEmail(userId string, email string, verified bool, local map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "ServerURL": rawUrl.Host}) - bodyPage := utils.NewHTMLTemplate("welcome_body", locale) + bodyPage := a.NewEmailTemplate("welcome_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.welcome_body.title", map[string]interface{}{"ServerURL": rawUrl.Host}) bodyPage.Props["Info"] = T("api.templates.welcome_body.info") @@ -167,7 +169,7 @@ func (a *App) SendPasswordChangeEmail(email, method, locale, siteURL string) *mo map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) - bodyPage := utils.NewHTMLTemplate("password_change_body", locale) + bodyPage := a.NewEmailTemplate("password_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.password_change_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.password_change_body.info", @@ -180,13 +182,13 @@ func (a *App) SendPasswordChangeEmail(email, method, locale, siteURL string) *mo return nil } -func SendUserAccessTokenAddedEmail(email, locale string) *model.AppError { +func (a *App) SendUserAccessTokenAddedEmail(email, locale string) *model.AppError { T := utils.GetUserTranslations(locale) subject := T("api.templates.user_access_token_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("password_change_body", locale) + bodyPage := a.NewEmailTemplate("password_change_body", locale) bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.user_access_token_body.info", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "SiteURL": utils.GetSiteURL()}) @@ -198,7 +200,7 @@ func SendUserAccessTokenAddedEmail(email, locale string) *model.AppError { return nil } -func SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, *model.AppError) { +func (a *App) SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, *model.AppError) { T := utils.GetUserTranslations(locale) @@ -207,7 +209,7 @@ func SendPasswordResetEmail(email string, token *model.Token, locale, siteURL st subject := T("api.templates.reset_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("reset_body", locale) + bodyPage := a.NewEmailTemplate("reset_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.reset_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.reset_body.info", nil) @@ -221,13 +223,13 @@ func SendPasswordResetEmail(email string, token *model.Token, locale, siteURL st return true, nil } -func SendMfaChangeEmail(email string, activated bool, locale, siteURL string) *model.AppError { +func (a *App) SendMfaChangeEmail(email string, activated bool, locale, siteURL string) *model.AppError { T := utils.GetUserTranslations(locale) subject := T("api.templates.mfa_change_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("mfa_change_body", locale) + bodyPage := a.NewEmailTemplate("mfa_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyText := "" @@ -258,7 +260,7 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []st "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE) + bodyPage := a.NewEmailTemplate("invite_body", model.DEFAULT_LOCALE) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body.info", @@ -288,3 +290,27 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []st } } } + +func (a *App) NewEmailTemplate(name, locale string) *utils.HTMLTemplate { + t := utils.NewHTMLTemplate(a.HTMLTemplates(), name) + + var localT i18n.TranslateFunc + if locale != "" { + localT = utils.GetUserTranslations(locale) + } else { + localT = utils.T + } + + t.Props["Footer"] = localT("api.templates.email_footer") + + if *a.Config().EmailSettings.FeedbackOrganization != "" { + t.Props["Organization"] = localT("api.templates.email_organization") + *a.Config().EmailSettings.FeedbackOrganization + } else { + t.Props["Organization"] = "" + } + + t.Html["EmailInfo"] = utils.TranslateAsHtml(localT, "api.templates.email_info", + map[string]interface{}{"SupportEmail": *a.Config().SupportSettings.SupportEmail, "SiteName": a.Config().TeamSettings.SiteName}) + + return t +} diff --git a/app/email_batching.go b/app/email_batching.go index 80e0966d5..bdacc65f5 100644 --- a/app/email_batching.go +++ b/app/email_batching.go @@ -236,7 +236,7 @@ func (a *App) sendBatchedEmailNotification(userId string, notifications []*batch "Day": tm.Day(), }) - body := utils.NewHTMLTemplate("post_batched_body", user.Locale) + body := a.NewEmailTemplate("post_batched_body", user.Locale) body.Props["SiteURL"] = *a.Config().ServiceSettings.SiteURL body.Props["Posts"] = template.HTML(contents) body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) @@ -250,9 +250,9 @@ func (a *App) renderBatchedPost(notification *batchedNotification, channel *mode // don't include message contents if email notification contents type is set to generic var template *utils.HTMLTemplate if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - template = utils.NewHTMLTemplate("post_batched_post_full", userLocale) + template = a.NewEmailTemplate("post_batched_post_full", userLocale) } else { - template = utils.NewHTMLTemplate("post_batched_post_generic", userLocale) + template = a.NewEmailTemplate("post_batched_post_generic", userLocale) } template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") diff --git a/app/notification.go b/app/notification.go index 386016250..7708270a4 100644 --- a/app/notification.go +++ b/app/notification.go @@ -413,10 +413,10 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, // only include message contents in notification email if email notification contents type is set to full var bodyPage *utils.HTMLTemplate if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - bodyPage = utils.NewHTMLTemplate("post_body_full", recipient.Locale) + bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale) bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc) } else { - bodyPage = utils.NewHTMLTemplate("post_body_generic", recipient.Locale) + bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale) } bodyPage.Props["SiteURL"] = utils.GetSiteURL() diff --git a/app/session.go b/app/session.go index 585b191ce..bf5f68fa3 100644 --- a/app/session.go +++ b/app/session.go @@ -247,7 +247,7 @@ func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAc l4g.Error(result.Err.Error()) } else { user := result.Data.(*model.User) - if err := SendUserAccessTokenAddedEmail(user.Email, user.Locale); err != nil { + if err := a.SendUserAccessTokenAddedEmail(user.Email, user.Locale); err != nil { l4g.Error(err.Error()) } } diff --git a/app/user.go b/app/user.go index 3bd549abe..a17521d9f 100644 --- a/app/user.go +++ b/app/user.go @@ -1065,7 +1065,7 @@ func (a *App) UpdateMfa(activate bool, userId, token string) *model.AppError { return } - if err := SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil { + if err := a.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil { l4g.Error(err.Error()) } }) @@ -1160,7 +1160,7 @@ func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppE return false, err } - if _, err := SendPasswordResetEmail(user.Email, token, user.Locale, siteURL); err != nil { + if _, err := a.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL); err != nil { return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError) } diff --git a/cmd/platform/user.go b/cmd/platform/user.go index ea654f89f..e5e068023 100644 --- a/cmd/platform/user.go +++ b/cmd/platform/user.go @@ -8,7 +8,6 @@ import ( "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" "github.com/spf13/cobra" ) @@ -260,8 +259,6 @@ func userInviteCmdF(cmd *cobra.Command, args []string) error { return err } - utils.InitHTML() - if len(args) < 2 { return errors.New("Expected at least two arguments. See help text for details.") } diff --git a/utils/html.go b/utils/html.go index fdba3e08d..02db8c97a 100644 --- a/utils/html.go +++ b/utils/html.go @@ -6,55 +6,60 @@ package utils import ( "bytes" "html/template" - "net/http" + "io" "reflect" + "sync/atomic" l4g "github.com/alecthomas/log4go" "github.com/fsnotify/fsnotify" "github.com/nicksnyder/go-i18n/i18n" ) -// Global storage for templates -var htmlTemplates *template.Template - -type HTMLTemplate struct { - TemplateName string - Props map[string]interface{} - Html map[string]template.HTML - Locale string +type HTMLTemplateWatcher struct { + templates atomic.Value + stop chan struct{} + stopped chan struct{} } -func InitHTML() { - InitHTMLWithDir("templates") -} +func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) { + templatesDir, _ := FindDir(directory) + l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) -func InitHTMLWithDir(dir string) { + ret := &HTMLTemplateWatcher{ + stop: make(chan struct{}), + stopped: make(chan struct{}), + } - if htmlTemplates != nil { - return + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err } - templatesDir, _ := FindDir(dir) - l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) - var err error - if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(T("api.api.init.parsing_templates.error"), err) + if err = watcher.Add(templatesDir); err != nil { + return nil, err } - // Watch the templates folder for changes. - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(T("web.create_dir.error"), err) + if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil { + return nil, err + } else { + ret.templates.Store(htmlTemplates) } go func() { + defer close(ret.stopped) + defer watcher.Close() + for { select { + case <-ret.stop: + return case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { l4g.Info(T("web.reparse_templates.info"), event.Name) - if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil { l4g.Error(T("web.parsing_templates.error"), err) + } else { + ret.templates.Store(htmlTemplates) } } case err := <-watcher.Errors: @@ -63,57 +68,42 @@ func InitHTMLWithDir(dir string) { } }() - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(T("web.watcher_fail.error"), err) - } + return ret, nil } -func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate { - return &HTMLTemplate{ - TemplateName: templateName, - Props: make(map[string]interface{}), - Html: make(map[string]template.HTML), - Locale: locale, - } +func (w *HTMLTemplateWatcher) Templates() *template.Template { + return w.templates.Load().(*template.Template) } -func (t *HTMLTemplate) addDefaultProps() { - var localT i18n.TranslateFunc - if len(t.Locale) > 0 { - localT = GetUserTranslations(t.Locale) - } else { - localT = T - } +func (w *HTMLTemplateWatcher) Close() { + close(w.stop) + <-w.stopped +} - t.Props["Footer"] = localT("api.templates.email_footer") +type HTMLTemplate struct { + Templates *template.Template + TemplateName string + Props map[string]interface{} + Html map[string]template.HTML +} - if *Cfg.EmailSettings.FeedbackOrganization != "" { - t.Props["Organization"] = localT("api.templates.email_organization") + *Cfg.EmailSettings.FeedbackOrganization - } else { - t.Props["Organization"] = "" +func NewHTMLTemplate(templates *template.Template, templateName string) *HTMLTemplate { + return &HTMLTemplate{ + Templates: templates, + TemplateName: templateName, + Props: make(map[string]interface{}), + Html: make(map[string]template.HTML), } - - 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 { - t.addDefaultProps() - var text bytes.Buffer - - if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil { - l4g.Error(T("api.api.render.error"), t.TemplateName, err) - } - + t.RenderToWriter(&text) return text.String() } -func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { - t.addDefaultProps() - - if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil { +func (t *HTMLTemplate) RenderToWriter(w io.Writer) error { + if err := t.Templates.ExecuteTemplate(w, t.TemplateName, t); err != nil { l4g.Error(T("api.api.render.error"), t.TemplateName, err) return err } diff --git a/utils/html_test.go b/utils/html_test.go index 8dc70242a..ba67189b6 100644 --- a/utils/html_test.go +++ b/utils/html_test.go @@ -4,50 +4,144 @@ package utils import ( + "bytes" "html/template" + "io/ioutil" + "os" + "path/filepath" "testing" + "time" + + "github.com/nicksnyder/go-i18n/i18n" + "github.com/nicksnyder/go-i18n/i18n/bundle" + "github.com/nicksnyder/go-i18n/i18n/language" + "github.com/nicksnyder/go-i18n/i18n/translation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" ) -func TestTranslateAsHtml(t *testing.T) { +var htmlTestTranslationBundle *bundle.Bundle + +func init() { + htmlTestTranslationBundle = bundle.New() + fooBold, _ := translation.NewTranslation(map[string]interface{}{ + "id": "foo.bold", + "translation": "<b>{{ .Foo }}</b>", + }) + htmlTestTranslationBundle.AddTranslation(&language.Language{Tag: "en"}, fooBold) +} + +func TestHTMLTemplateWatcher(t *testing.T) { TranslationsPreInit() - translateFunc := TfuncWithFallback("en") + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) - 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) - } + require.NoError(t, os.Mkdir(filepath.Join(dir, "templates"), 0700)) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}foo{{ end }}`), 0600)) + + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(dir) - 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": "<b>YOUR TEAM HERE</b>"}); actual != template.HTML(expected) { - t.Fatalf("Incorrectly translated template, got %v, expected %v", actual, expected) + watcher, err := NewHTMLTemplateWatcher("templates") + require.NotNil(t, watcher) + require.NoError(t, err) + defer watcher.Close() + + tpl := NewHTMLTemplate(watcher.Templates(), "foo") + assert.Equal(t, "foo", tpl.Render()) + + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}bar{{ end }}`), 0600)) + + for i := 0; i < 30; i++ { + tpl = NewHTMLTemplate(watcher.Templates(), "foo") + if tpl.Render() == "bar" { + break + } + time.Sleep(time.Millisecond * 50) } + assert.Equal(t, "bar", tpl.Render()) } -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) - } +func TestHTMLTemplateWatcher_BadDirectory(t *testing.T) { + TranslationsPreInit() + watcher, err := NewHTMLTemplateWatcher("notarealdirectory") + assert.Nil(t, watcher) + assert.Error(t, err) +} - input = "<b>abc</b>" - expected = "<b>abc</b>" - if actual := escapeForHtml(input).(string); actual != expected { - t.Fatalf("incorrectly escaped %v, got %v expected %v", input, actual, expected) - } +func TestHTMLTemplate(t *testing.T) { + tpl := template.New("test") + _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Props.Bar }}{{ end }}`) + require.NoError(t, err) - inputMap := map[string]interface{}{ - "abc": "abc", - "123": "<b>123</b>", - } - 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) + htmlTemplate := NewHTMLTemplate(tpl, "foo") + htmlTemplate.Props["Bar"] = "bar" + assert.Equal(t, "foobar", htmlTemplate.Render()) + + buf := &bytes.Buffer{} + require.NoError(t, htmlTemplate.RenderToWriter(buf)) + assert.Equal(t, "foobar", buf.String()) +} + +func TestHTMLTemplate_RenderError(t *testing.T) { + tpl := template.New("test") + _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Foo.Bar }}bar{{ end }}`) + require.NoError(t, err) + + htmlTemplate := NewHTMLTemplate(tpl, "foo") + assert.Equal(t, "foo", htmlTemplate.Render()) + + buf := &bytes.Buffer{} + assert.Error(t, htmlTemplate.RenderToWriter(buf)) + assert.Equal(t, "foo", buf.String()) +} + +func TestTranslateAsHtml(t *testing.T) { + assert.EqualValues(t, "<b><i>foo</i></b>", TranslateAsHtml(i18n.TranslateFunc(htmlTestTranslationBundle.MustTfunc("en")), "foo.bold", map[string]interface{}{ + "Foo": "<i>foo</i>", + })) +} + +func TestEscapeForHtml(t *testing.T) { + for name, tc := range map[string]struct { + In interface{} + Expected interface{} + }{ + "NoHTML": { + In: "abc", + Expected: "abc", + }, + "String": { + In: "<b>abc</b>", + Expected: "<b>abc</b>", + }, + "StringPointer": { + In: model.NewString("<b>abc</b>"), + Expected: "<b>abc</b>", + }, + "Map": { + In: map[string]interface{}{ + "abc": "abc", + "123": "<b>123</b>", + }, + Expected: map[string]interface{}{ + "abc": "abc", + "123": "<b>123</b>", + }, + }, + "Unsupported": { + In: struct{ string }{"<b>abc</b>"}, + Expected: "", + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.Expected, escapeForHtml(tc.In)) + }) } } diff --git a/web/web.go b/web/web.go index 1724fd3f2..40cc4284b 100644 --- a/web/web.go +++ b/web/web.go @@ -81,13 +81,12 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { } return true - } func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { w.Header().Set("Cache-Control", "no-store") - page := utils.NewHTMLTemplate("unsupported_browser", c.Locale) + page := utils.NewHTMLTemplate(c.App.HTMLTemplates(), "unsupported_browser") page.Props["Title"] = c.T("web.error.unsupported_browser.title") page.Props["Message"] = c.T("web.error.unsupported_browser.message") page.RenderToWriter(w) |