From fdbd64611383f8b78dc76765f6edfa3e40a3a0bf Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 8 Apr 2023 21:15:22 +0800 Subject: [PATCH] Group template helper functions, remove `Printf`, improve template error messages (#23982) Follow #23328 Major changes: * Group the function in `templates/help.go` by their purposes. It could make future work easier. * Remove the `Printf` helper function, there is already a builtin `printf`. * Remove `DiffStatsWidth`, replace with `Eval` in template * Rename the `NewTextFuncMap` to `mailSubjectTextFuncMap`, it's for subject text template only, no need to make it support HTML functions. ---- And fine tune template error messages, to make it more friendly to developers and users. ![image](https://user-images.githubusercontent.com/2114189/230714245-4fd202d1-2b25-41b2-8be5-03c5fee45091.png) ![image](https://user-images.githubusercontent.com/2114189/230714277-66783577-2a03-49d5-8e8c-ceba5e07a2d4.png) --------- Co-authored-by: silverwind --- modules/context/context.go | 8 +- modules/templates/dynamic.go | 7 - modules/templates/helper.go | 453 ++++++++---------- modules/templates/htmlrenderer.go | 29 +- modules/templates/mailer.go | 43 +- modules/test/context_tests.go | 4 +- routers/web/auth/oauth.go | 11 +- routers/web/swagger_json.go | 14 +- templates/repo/diff/comments.tmpl | 4 +- templates/repo/diff/stats.tmpl | 3 +- templates/repo/issue/view_content.tmpl | 4 +- .../repo/issue/view_content/comments.tmpl | 12 +- .../repo/issue/view_content/context_menu.tmpl | 4 +- .../view_content/reference_issue_dialog.tmpl | 2 +- templates/repo/migrate/migrate.tmpl | 4 +- 15 files changed, 307 insertions(+), 295 deletions(-) diff --git a/modules/context/context.go b/modules/context/context.go index 04f8a9bd3d..bd561be0f5 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -47,7 +47,7 @@ import ( // Render represents a template render type Render interface { - TemplateLookup(tmpl string) *template.Template + TemplateLookup(tmpl string) (*template.Template, error) HTML(w io.Writer, status int, name string, data interface{}) error } @@ -228,7 +228,7 @@ func (ctx *Context) HTML(status int, name base.TplName) { } if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil { if status == http.StatusInternalServerError && name == base.TplName("status/500") { - ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template") + ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.") return } if execErr, ok := err.(texttemplate.ExecError); ok { @@ -247,7 +247,7 @@ func (ctx *Context) HTML(status int, name base.TplName) { if errorTemplateName != string(name) { filename += " (subtemplate of " + string(name) + ")" } - err = fmt.Errorf("%w\nin template file %s:\n%s", err, filename, templates.GetLineFromTemplate(errorTemplateName, line, target, pos)) + err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos)) } else { filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl") if filenameErr != nil { @@ -256,7 +256,7 @@ func (ctx *Context) HTML(status int, name base.TplName) { if execErr.Name != string(name) { filename += " (subtemplate of " + string(name) + ")" } - err = fmt.Errorf("%w\nin template file %s", err, filename) + err = fmt.Errorf("failed to render %s, error: %w", filename, err) } } ctx.ServerError("Render failed", err) diff --git a/modules/templates/dynamic.go b/modules/templates/dynamic.go index 7d25a61fed..2f4f542e72 100644 --- a/modules/templates/dynamic.go +++ b/modules/templates/dynamic.go @@ -6,20 +6,13 @@ package templates import ( - "html/template" "io/fs" "os" "path/filepath" - texttmpl "text/template" "code.gitea.io/gitea/modules/setting" ) -var ( - subjectTemplates = texttmpl.New("") - bodyTemplates = template.New("") -) - // GetAsset returns asset content via name func GetAsset(name string) ([]byte, error) { bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name)) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 407b2c64f5..40a79d9578 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -18,7 +18,6 @@ import ( "reflect" "regexp" "strings" - texttmpl "text/template" "time" "unicode" @@ -55,6 +54,134 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`) // NewFuncMap returns functions for injecting to templates func NewFuncMap() []template.FuncMap { return []template.FuncMap{map[string]interface{}{ + // ----------------------------------------------------------------- + // html/template related functions + "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. + "Eval": Eval, + "Safe": Safe, + "Escape": html.EscapeString, + "QueryEscape": url.QueryEscape, + "JSEscape": template.JSEscapeString, + "Str2html": Str2html, // TODO: rename it to SanitizeHTML + "URLJoin": util.URLJoin, + + "PathEscape": url.PathEscape, + "PathEscapeSegments": util.PathEscapeSegments, + + // ----------------------------------------------------------------- + // string / json + "Join": strings.Join, + "DotEscape": DotEscape, + "HasPrefix": strings.HasPrefix, + "EllipsisString": base.EllipsisString, + + "Json": func(in interface{}) string { + out, err := json.Marshal(in) + if err != nil { + return "" + } + return string(out) + }, + "JsonPrettyPrint": func(in string) string { + var out bytes.Buffer + err := json.Indent(&out, []byte(in), "", " ") + if err != nil { + return "" + } + return out.String() + }, + + // ----------------------------------------------------------------- + // svg / avatar / icon + "svg": svg.RenderHTML, + "avatar": Avatar, + "avatarHTML": AvatarHTML, + "avatarByAction": AvatarByAction, + "avatarByEmail": AvatarByEmail, + "repoAvatar": RepoAvatar, + "EntryIcon": base.EntryIcon, + "MigrationIcon": MigrationIcon, + "ActionIcon": ActionIcon, + + "SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML { + // if needed + if len(normSort) == 0 || len(urlSort) == 0 { + return "" + } + + if len(urlSort) == 0 && isDefault { + // if sort is sorted as default add arrow tho this table header + if isDefault { + return svg.RenderHTML("octicon-triangle-down", 16) + } + } else { + // if sort arg is in url test if it correlates with column header sort arguments + // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev) + if urlSort == normSort { + // the table is sorted with this header normal + return svg.RenderHTML("octicon-triangle-up", 16) + } else if urlSort == revSort { + // the table is sorted with this header reverse + return svg.RenderHTML("octicon-triangle-down", 16) + } + } + // the table is NOT sorted with this header + return "" + }, + + // ----------------------------------------------------------------- + // time / number / format + "FileSize": base.FileSize, + "LocaleNumber": LocaleNumber, + "CountFmt": base.FormatNumberSI, + "TimeSince": timeutil.TimeSince, + "TimeSinceUnix": timeutil.TimeSinceUnix, + "Sec2Time": util.SecToTime, + "DateFmtLong": func(t time.Time) string { + return t.Format(time.RFC1123Z) + }, + "LoadTimes": func(startTime time.Time) string { + return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" + }, + + // ----------------------------------------------------------------- + // slice + "containGeneric": func(arr, v interface{}) bool { + arrV := reflect.ValueOf(arr) + if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String { + return strings.Contains(arr.(string), v.(string)) + } + if arrV.Kind() == reflect.Slice { + for i := 0; i < arrV.Len(); i++ { + iV := arrV.Index(i) + if !iV.CanInterface() { + continue + } + if iV.Interface() == v { + return true + } + } + } + return false + }, + "contain": func(s []int64, id int64) bool { + for i := 0; i < len(s); i++ { + if s[i] == id { + return true + } + } + return false + }, + "Iterate": func(arg interface{}) (items []int64) { + count, _ := util.ToInt64(arg) + for i := int64(0); i < count; i++ { + items = append(items, i) + } + return items + }, + + // ----------------------------------------------------------------- + // setting "AppName": func() string { return setting.AppName }, @@ -89,56 +216,12 @@ func NewFuncMap() []template.FuncMap { "ShowFooterTemplateLoadTime": func() bool { return setting.ShowFooterTemplateLoadTime }, - "LoadTimes": func(startTime time.Time) string { - return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" - }, "AllowedReactions": func() []string { return setting.UI.Reactions }, "CustomEmojis": func() map[string]string { return setting.UI.CustomEmojisMap }, - "Safe": Safe, - "JSEscape": JSEscape, - "Str2html": Str2html, - "TimeSince": timeutil.TimeSince, - "TimeSinceUnix": timeutil.TimeSinceUnix, - "FileSize": base.FileSize, - "LocaleNumber": LocaleNumber, - "EntryIcon": base.EntryIcon, - "MigrationIcon": MigrationIcon, - "ActionIcon": ActionIcon, - "DateFmtLong": func(t time.Time) string { - return t.Format(time.RFC1123Z) - }, - "CountFmt": base.FormatNumberSI, - "EllipsisString": base.EllipsisString, - "DiffLineTypeToStr": DiffLineTypeToStr, - "ShortSha": base.ShortSha, - "ActionContent2Commits": ActionContent2Commits, - "PathEscape": url.PathEscape, - "PathEscapeSegments": util.PathEscapeSegments, - "URLJoin": util.URLJoin, - "RenderCommitMessage": RenderCommitMessage, - "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject, - "RenderCommitBody": RenderCommitBody, - "RenderCodeBlock": RenderCodeBlock, - "RenderIssueTitle": RenderIssueTitle, - "RenderEmoji": RenderEmoji, - "RenderEmojiPlain": emoji.ReplaceAliases, - "ReactionToEmoji": ReactionToEmoji, - "RenderNote": RenderNote, - "RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML { - output, err := markdown.RenderString(&markup.RenderContext{ - Ctx: ctx, - URLPrefix: setting.AppSubURL, - }, input) - if err != nil { - log.Error("RenderString: %v", err) - } - return template.HTML(output) - }, - "IsMultilineCommitMessage": IsMultilineCommitMessage, "ThemeColorMetaTag": func() string { return setting.UI.ThemeColorMetaTag }, @@ -157,6 +240,82 @@ func NewFuncMap() []template.FuncMap { "EnableTimetracking": func() bool { return setting.Service.EnableTimetracking }, + "DisableGitHooks": func() bool { + return setting.DisableGitHooks + }, + "DisableWebhooks": func() bool { + return setting.DisableWebhooks + }, + "DisableImportLocal": func() bool { + return !setting.ImportLocalPaths + }, + "DefaultTheme": func() string { + return setting.UI.DefaultTheme + }, + "NotificationSettings": func() map[string]interface{} { + return map[string]interface{}{ + "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), + "TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond), + "MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond), + "EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond), + } + }, + "MermaidMaxSourceCharacters": func() int { + return setting.MermaidMaxSourceCharacters + }, + + // ----------------------------------------------------------------- + // render + "RenderCommitMessage": RenderCommitMessage, + "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject, + + "RenderCommitBody": RenderCommitBody, + "RenderCodeBlock": RenderCodeBlock, + "RenderIssueTitle": RenderIssueTitle, + "RenderEmoji": RenderEmoji, + "RenderEmojiPlain": emoji.ReplaceAliases, + "ReactionToEmoji": ReactionToEmoji, + "RenderNote": RenderNote, + + "RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML { + output, err := markdown.RenderString(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: setting.AppSubURL, + }, input) + if err != nil { + log.Error("RenderString: %v", err) + } + return template.HTML(output) + }, + "RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML { + return template.HTML(RenderLabel(ctx, label)) + }, + "RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { + htmlCode := `` + for _, label := range labels { + // Protect against nil value in labels - shouldn't happen but would cause a panic if so + if label == nil { + continue + } + htmlCode += fmt.Sprintf("%s ", + repoLink, label.ID, RenderLabel(ctx, label)) + } + htmlCode += "" + return template.HTML(htmlCode) + }, + + // ----------------------------------------------------------------- + // misc + "DiffLineTypeToStr": DiffLineTypeToStr, + "ShortSha": base.ShortSha, + "ActionContent2Commits": ActionContent2Commits, + "IsMultilineCommitMessage": IsMultilineCommitMessage, + "CommentMustAsDiff": gitdiff.CommentMustAsDiff, + "MirrorRemoteAddress": mirrorRemoteAddress, + + "ParseDeadline": func(deadline string) []string { + return strings.Split(deadline, "|") + }, "FilenameIsImage": func(filename string) bool { mimeType := mime.TypeByExtension(filepath.Ext(filename)) return strings.HasPrefix(mimeType, "image/") @@ -191,142 +350,6 @@ func NewFuncMap() []template.FuncMap { } return path }, - "DiffStatsWidth": func(adds, dels int) string { - return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100) - }, - "Json": func(in interface{}) string { - out, err := json.Marshal(in) - if err != nil { - return "" - } - return string(out) - }, - "JsonPrettyPrint": func(in string) string { - var out bytes.Buffer - err := json.Indent(&out, []byte(in), "", " ") - if err != nil { - return "" - } - return out.String() - }, - "DisableGitHooks": func() bool { - return setting.DisableGitHooks - }, - "DisableWebhooks": func() bool { - return setting.DisableWebhooks - }, - "DisableImportLocal": func() bool { - return !setting.ImportLocalPaths - }, - "Printf": fmt.Sprintf, - "Escape": Escape, - "Sec2Time": util.SecToTime, - "ParseDeadline": func(deadline string) []string { - return strings.Split(deadline, "|") - }, - "DefaultTheme": func() string { - return setting.UI.DefaultTheme - }, - "dict": dict, - "CommentMustAsDiff": gitdiff.CommentMustAsDiff, - "MirrorRemoteAddress": mirrorRemoteAddress, - "NotificationSettings": func() map[string]interface{} { - return map[string]interface{}{ - "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), - "TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond), - "MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond), - "EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond), - } - }, - "containGeneric": func(arr, v interface{}) bool { - arrV := reflect.ValueOf(arr) - if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String { - return strings.Contains(arr.(string), v.(string)) - } - - if arrV.Kind() == reflect.Slice { - for i := 0; i < arrV.Len(); i++ { - iV := arrV.Index(i) - if !iV.CanInterface() { - continue - } - if iV.Interface() == v { - return true - } - } - } - - return false - }, - "contain": func(s []int64, id int64) bool { - for i := 0; i < len(s); i++ { - if s[i] == id { - return true - } - } - return false - }, - "svg": svg.RenderHTML, - "avatar": Avatar, - "avatarHTML": AvatarHTML, - "avatarByAction": AvatarByAction, - "avatarByEmail": AvatarByEmail, - "repoAvatar": RepoAvatar, - "SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML { - // if needed - if len(normSort) == 0 || len(urlSort) == 0 { - return "" - } - - if len(urlSort) == 0 && isDefault { - // if sort is sorted as default add arrow tho this table header - if isDefault { - return svg.RenderHTML("octicon-triangle-down", 16) - } - } else { - // if sort arg is in url test if it correlates with column header sort arguments - // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev) - if urlSort == normSort { - // the table is sorted with this header normal - return svg.RenderHTML("octicon-triangle-up", 16) - } else if urlSort == revSort { - // the table is sorted with this header reverse - return svg.RenderHTML("octicon-triangle-down", 16) - } - } - // the table is NOT sorted with this header - return "" - }, - "RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML { - return template.HTML(RenderLabel(ctx, label)) - }, - "RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { - htmlCode := `` - for _, label := range labels { - // Protect against nil value in labels - shouldn't happen but would cause a panic if so - if label == nil { - continue - } - htmlCode += fmt.Sprintf("%s ", - repoLink, label.ID, RenderLabel(ctx, label)) - } - htmlCode += "" - return template.HTML(htmlCode) - }, - "MermaidMaxSourceCharacters": func() int { - return setting.MermaidMaxSourceCharacters - }, - "Join": strings.Join, - "QueryEscape": url.QueryEscape, - "DotEscape": DotEscape, - "Iterate": func(arg interface{}) (items []int64) { - count, _ := util.ToInt64(arg) - for i := int64(0); i < count; i++ { - items = append(items, i) - } - return items - }, - "HasPrefix": strings.HasPrefix, "CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string { var curBranch string if repo.ID != baseRepo.ID { @@ -340,45 +363,6 @@ func NewFuncMap() []template.FuncMap { curBranch, ) }, - "Eval": Eval, - }} -} - -// NewTextFuncMap returns functions for injecting to text templates -// It's a subset of those used for HTML and other templates -func NewTextFuncMap() []texttmpl.FuncMap { - return []texttmpl.FuncMap{map[string]interface{}{ - "AppName": func() string { - return setting.AppName - }, - "AppSubUrl": func() string { - return setting.AppSubURL - }, - "AppUrl": func() string { - return setting.AppURL - }, - "AppVer": func() string { - return setting.AppVer - }, - "AppDomain": func() string { // documented in mail-templates.md - return setting.Domain - }, - "TimeSince": timeutil.TimeSince, - "TimeSinceUnix": timeutil.TimeSinceUnix, - "DateFmtLong": func(t time.Time) string { - return t.Format(time.RFC1123Z) - }, - "EllipsisString": base.EllipsisString, - "URLJoin": util.URLJoin, - "Printf": fmt.Sprintf, - "Escape": Escape, - "Sec2Time": util.SecToTime, - "ParseDeadline": func(deadline string) []string { - return strings.Split(deadline, "|") - }, - "dict": dict, - "QueryEscape": url.QueryEscape, - "Eval": Eval, }} } @@ -457,16 +441,6 @@ func Str2html(raw string) template.HTML { return template.HTML(markup.Sanitize(raw)) } -// Escape escapes a HTML string -func Escape(raw string) string { - return html.EscapeString(raw) -} - -// JSEscape escapes a JS string -func JSEscape(raw string) string { - return template.JSEscapeString(raw) -} - // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls func DotEscape(raw string) string { return strings.ReplaceAll(raw, ".", "\u200d.\u200d") @@ -771,25 +745,6 @@ func MigrationIcon(hostname string) string { } } -func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) { - // Split template into subject and body - var subjectContent []byte - bodyContent := content - loc := mailSubjectSplit.FindIndex(content) - if loc != nil { - subjectContent = content[0:loc[0]] - bodyContent = content[loc[1]:] - } - if _, err := stpl.New(name). - Parse(string(subjectContent)); err != nil { - log.Warn("Failed to parse template [%s/subject]: %v", name, err) - } - if _, err := btpl.New(name). - Parse(string(bodyContent)); err != nil { - log.Warn("Failed to parse template [%s/body]: %v", name, err) - } -} - type remoteAddress struct { Address string Username string diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index fd985edc64..f2c818798c 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -6,6 +6,7 @@ package templates import ( "bytes" "context" + "errors" "fmt" "html/template" "io" @@ -15,9 +16,11 @@ import ( "strconv" "strings" "sync/atomic" + texttemplate "text/template" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/watcher" ) @@ -34,6 +37,8 @@ type HTMLRender struct { templates atomic.Pointer[template.Template] } +var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") + func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error { if respWriter, ok := w.(http.ResponseWriter); ok { if respWriter.Header().Get("Content-Type") == "" { @@ -41,11 +46,23 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{} } respWriter.WriteHeader(status) } - return h.templates.Load().ExecuteTemplate(w, name, data) + t, err := h.TemplateLookup(name) + if err != nil { + return texttemplate.ExecError{Name: name, Err: err} + } + return t.Execute(w, data) } -func (h *HTMLRender) TemplateLookup(t string) *template.Template { - return h.templates.Load().Lookup(t) +func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) { + tmpls := h.templates.Load() + if tmpls == nil { + return nil, ErrTemplateNotInitialized + } + tmpl := tmpls.Lookup(name) + if tmpl == nil { + return nil, util.ErrNotExist + } + return tmpl, nil } func (h *HTMLRender) CompileTemplates() error { @@ -237,6 +254,12 @@ func GetLineFromTemplate(templateName string, targetLineNum int, target string, } } + // FIXME: this algorithm could provide incorrect results and mislead the developers. + // For example: Undefined function "file" in template ..... + // {{Func .file.Addition file.Deletion .file.Addition}} + // ^^^^ ^(the real error is here) + // The pointer is added to the first one, but the second one is the real incorrect one. + // // If there is a provided target to look for in the line add a pointer to it // e.g. ^^^^^^^ if target != "" { diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index a257e7c1da..d0c49e1025 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -11,16 +11,53 @@ import ( "strings" texttmpl "text/template" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/watcher" ) +// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject +func mailSubjectTextFuncMap() texttmpl.FuncMap { + return texttmpl.FuncMap{ + "dict": dict, + "Eval": Eval, + + "EllipsisString": base.EllipsisString, + "AppName": func() string { + return setting.AppName + }, + "AppDomain": func() string { // documented in mail-templates.md + return setting.Domain + }, + } +} + +func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) { + // Split template into subject and body + var subjectContent []byte + bodyContent := content + loc := mailSubjectSplit.FindIndex(content) + if loc != nil { + subjectContent = content[0:loc[0]] + bodyContent = content[loc[1]:] + } + if _, err := stpl.New(name). + Parse(string(subjectContent)); err != nil { + log.Warn("Failed to parse template [%s/subject]: %v", name, err) + } + if _, err := btpl.New(name). + Parse(string(bodyContent)); err != nil { + log.Warn("Failed to parse template [%s/body]: %v", name, err) + } +} + // Mailer provides the templates required for sending notification mails. func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) { - for _, funcs := range NewTextFuncMap() { - subjectTemplates.Funcs(funcs) - } + subjectTemplates := texttmpl.New("") + bodyTemplates := template.New("") + + subjectTemplates.Funcs(mailSubjectTextFuncMap()) for _, funcs := range NewFuncMap() { bodyTemplates.Funcs(funcs) } diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index 5e66049535..94dbd2f290 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -133,8 +133,8 @@ func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error type mockRender struct{} -func (tr *mockRender) TemplateLookup(tmpl string) *template.Template { - return nil +func (tr *mockRender) TemplateLookup(tmpl string) (*template.Template, error) { + return nil, nil } func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ interface{}) error { diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 7de63dbe94..b3c4a234c1 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -578,12 +578,15 @@ func GrantApplicationOAuth(ctx *context.Context) { // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities func OIDCWellKnown(ctx *context.Context) { - t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown") + t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown") + if err != nil { + ctx.ServerError("unable to find template", err) + return + } ctx.Resp.Header().Set("Content-Type", "application/json") ctx.Data["SigningKey"] = oauth2.DefaultSigningKey - if err := t.Execute(ctx.Resp, ctx.Data); err != nil { - log.Error("%v", err) - ctx.Error(http.StatusInternalServerError) + if err = t.Execute(ctx.Resp, ctx.Data); err != nil { + ctx.ServerError("unable to execute template", err) } } diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go index 2d626c558e..1844b90d95 100644 --- a/routers/web/swagger_json.go +++ b/routers/web/swagger_json.go @@ -4,11 +4,8 @@ package web import ( - "net/http" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" ) // tplSwaggerV1Json swagger v1 json template @@ -16,10 +13,13 @@ const tplSwaggerV1Json base.TplName = "swagger/v1_json" // SwaggerV1Json render swagger v1 json func SwaggerV1Json(ctx *context.Context) { - t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json)) + t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json)) + if err != nil { + ctx.ServerError("unable to find template", err) + return + } ctx.Resp.Header().Set("Content-Type", "application/json") - if err := t.Execute(ctx.Resp, ctx.Data); err != nil { - log.Error("%v", err) - ctx.Error(http.StatusInternalServerError) + if err = t.Execute(ctx.Resp, ctx.Data); err != nil { + ctx.ServerError("unable to execute template", err) } } diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 5c26e7815f..5bcaeeb463 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -42,7 +42,7 @@ {{end}} {{end}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}} {{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}} @@ -60,7 +60,7 @@ {{$reactions := .Reactions.GroupByType}} {{if $reactions}}
- {{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
{{end}} diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl index 1e466d6b46..ff7e90d29c 100644 --- a/templates/repo/diff/stats.tmpl +++ b/templates/repo/diff/stats.tmpl @@ -1,4 +1,5 @@ {{Eval .file.Addition "+" .file.Deletion}} -
+ {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}} +
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 7c94d00fe4..a5d9b0bde0 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -64,7 +64,7 @@ {{end}} {{end}} {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}} {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}} {{end}} @@ -86,7 +86,7 @@ {{$reactions := .Issue.Reactions.GroupByType}} {{if $reactions}}
- {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 52b453d0dd..91bef4fe1a 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -64,7 +64,7 @@ {{end}} {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} {{end}} @@ -86,7 +86,7 @@ {{$reactions := .Reactions.GroupByType}} {{if $reactions}}
- {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
{{end}} @@ -436,7 +436,7 @@ {{end}} {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} {{end}} @@ -458,7 +458,7 @@ {{$reactions := .Reactions.GroupByType}} {{if $reactions}}
- {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
{{end}} @@ -563,7 +563,7 @@ {{end}} {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} {{end}} @@ -582,7 +582,7 @@ {{$reactions := .Reactions.GroupByType}} {{if $reactions}}
- {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
{{end}} diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl index 0cc207afd4..aa9d52b67a 100644 --- a/templates/repo/issue/view_content/context_menu.tmpl +++ b/templates/repo/issue/view_content/context_menu.tmpl @@ -6,9 +6,9 @@
-
+ {{.CsrfTokenHtml}}
diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl index 9663b85eba..44769e60bd 100644 --- a/templates/repo/migrate/migrate.tmpl +++ b/templates/repo/migrate/migrate.tmpl @@ -6,13 +6,13 @@