diff options
Diffstat (limited to 'utils')
-rw-r--r-- | utils/html.go | 114 | ||||
-rw-r--r-- | utils/html_test.go | 158 |
2 files changed, 178 insertions, 94 deletions
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)) + }) } } |