aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2023-04-08 21:15:22 +0800
committerGitHub <noreply@github.com>2023-04-08 21:15:22 +0800
commitfdbd64611383f8b78dc76765f6edfa3e40a3a0bf (patch)
treedae3fa75aeaf28a8abd9565fb544ae8b72d9e530
parentcf5a281fdc23543584a3a06fcfcf796b08425a79 (diff)
downloadgitea-fdbd64611383f8b78dc76765f6edfa3e40a3a0bf.tar.gz
gitea-fdbd64611383f8b78dc76765f6edfa3e40a3a0bf.zip
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 <me@silverwind.io>
-rw-r--r--modules/context/context.go8
-rw-r--r--modules/templates/dynamic.go7
-rw-r--r--modules/templates/helper.go433
-rw-r--r--modules/templates/htmlrenderer.go29
-rw-r--r--modules/templates/mailer.go43
-rw-r--r--modules/test/context_tests.go4
-rw-r--r--routers/web/auth/oauth.go11
-rw-r--r--routers/web/swagger_json.go14
-rw-r--r--templates/repo/diff/comments.tmpl4
-rw-r--r--templates/repo/diff/stats.tmpl3
-rw-r--r--templates/repo/issue/view_content.tmpl4
-rw-r--r--templates/repo/issue/view_content/comments.tmpl12
-rw-r--r--templates/repo/issue/view_content/context_menu.tmpl4
-rw-r--r--templates/repo/issue/view_content/reference_issue_dialog.tmpl2
-rw-r--r--templates/repo/migrate/migrate.tmpl4
15 files changed, 297 insertions, 285 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,58 +240,6 @@ func NewFuncMap() []template.FuncMap {
"EnableTimetracking": func() bool {
return setting.Service.EnableTimetracking
},
- "FilenameIsImage": func(filename string) bool {
- mimeType := mime.TypeByExtension(filepath.Ext(filename))
- return strings.HasPrefix(mimeType, "image/")
- },
- "TabSizeClass": func(ec interface{}, filename string) string {
- var (
- value *editorconfig.Editorconfig
- ok bool
- )
- if ec != nil {
- if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
- return "tab-size-8"
- }
- def, err := value.GetDefinitionForFilename(filename)
- if err != nil {
- log.Error("tab size class: getting definition for filename: %v", err)
- return "tab-size-8"
- }
- if def.TabWidth > 0 {
- return fmt.Sprintf("tab-size-%d", def.TabWidth)
- }
- }
- return "tab-size-8"
- },
- "SubJumpablePath": func(str string) []string {
- var path []string
- index := strings.LastIndex(str, "/")
- if index != -1 && index != len(str) {
- path = append(path, str[0:index+1], str[index+1:])
- } else {
- path = append(path, str)
- }
- 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
},
@@ -218,18 +249,9 @@ func NewFuncMap() []template.FuncMap {
"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),
@@ -238,64 +260,32 @@ func NewFuncMap() []template.FuncMap {
"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))
- }
+ "MermaidMaxSourceCharacters": func() int {
+ return setting.MermaidMaxSourceCharacters
+ },
- 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
- }
- }
- }
+ // -----------------------------------------------------------------
+ // render
+ "RenderCommitMessage": RenderCommitMessage,
+ "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
- 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 ""
- }
+ "RenderCommitBody": RenderCommitBody,
+ "RenderCodeBlock": RenderCodeBlock,
+ "RenderIssueTitle": RenderIssueTitle,
+ "RenderEmoji": RenderEmoji,
+ "RenderEmojiPlain": emoji.ReplaceAliases,
+ "ReactionToEmoji": ReactionToEmoji,
+ "RenderNote": RenderNote,
- 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)
- }
+ "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)
}
- // the table is NOT sorted with this header
- return ""
+ return template.HTML(output)
},
"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
return template.HTML(RenderLabel(ctx, label))
@@ -313,20 +303,53 @@ func NewFuncMap() []template.FuncMap {
htmlCode += "</span>"
return template.HTML(htmlCode)
},
- "MermaidMaxSourceCharacters": func() int {
- return setting.MermaidMaxSourceCharacters
+
+ // -----------------------------------------------------------------
+ // misc
+ "DiffLineTypeToStr": DiffLineTypeToStr,
+ "ShortSha": base.ShortSha,
+ "ActionContent2Commits": ActionContent2Commits,
+ "IsMultilineCommitMessage": IsMultilineCommitMessage,
+ "CommentMustAsDiff": gitdiff.CommentMustAsDiff,
+ "MirrorRemoteAddress": mirrorRemoteAddress,
+
+ "ParseDeadline": func(deadline string) []string {
+ return strings.Split(deadline, "|")
},
- "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)
+ "FilenameIsImage": func(filename string) bool {
+ mimeType := mime.TypeByExtension(filepath.Ext(filename))
+ return strings.HasPrefix(mimeType, "image/")
+ },
+ "TabSizeClass": func(ec interface{}, filename string) string {
+ var (
+ value *editorconfig.Editorconfig
+ ok bool
+ )
+ if ec != nil {
+ if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
+ return "tab-size-8"
+ }
+ def, err := value.GetDefinitionForFilename(filename)
+ if err != nil {
+ log.Error("tab size class: getting definition for filename: %v", err)
+ return "tab-size-8"
+ }
+ if def.TabWidth > 0 {
+ return fmt.Sprintf("tab-size-%d", def.TabWidth)
+ }
}
- return items
+ return "tab-size-8"
+ },
+ "SubJumpablePath": func(str string) []string {
+ var path []string
+ index := strings.LastIndex(str, "/")
+ if index != -1 && index != len(str) {
+ path = append(path, str[0:index+1], str[index+1:])
+ } else {
+ path = append(path, str)
+ }
+ return path
},
- "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 @@
</div>
{{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))}}
</div>
</div>
@@ -60,7 +60,7 @@
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment 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}}
</div>
{{end}}
</div>
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}}
<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{.root.locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
- <div class="diff-stats-add-bar" style="width: {{DiffStatsWidth .file.Addition .file.Deletion}}%"></div>
+ {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
+ <div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
</span>
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}}
</div>
@@ -86,7 +86,7 @@
{{$reactions := .Issue.Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment reactions" role="note">
- {{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}}
</div>
{{end}}
</div>
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 @@
</div>
{{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}}
</div>
@@ -86,7 +86,7 @@
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment reactions" role="note">
- {{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}}
</div>
{{end}}
</div>
@@ -436,7 +436,7 @@
</div>
{{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}}
</div>
@@ -458,7 +458,7 @@
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment 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}}
</div>
{{end}}
</div>
@@ -563,7 +563,7 @@
</div>
{{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}}
</div>
@@ -582,7 +582,7 @@
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment 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}}
</div>
{{end}}
</div>
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 @@
<div class="menu">
{{$referenceUrl := ""}}
{{if .issue}}
- {{$referenceUrl = Printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
+ {{$referenceUrl = printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
{{else}}
- {{$referenceUrl = Printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
+ {{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
{{end}}
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}</div>
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}</div>
diff --git a/templates/repo/issue/view_content/reference_issue_dialog.tmpl b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
index e877ffb688..1bb6366ea4 100644
--- a/templates/repo/issue/view_content/reference_issue_dialog.tmpl
+++ b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
@@ -3,7 +3,7 @@
{{.locale.Tr "repo.issues.context.reference_issue"}}
</div>
<div class="content" style="text-align:left">
- <form class="ui form" action="{{Printf "%s/issues/new" .Repository.Link}}" method="post">
+ <form class="ui form" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="ui segment content">
<div class="field">
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 @@
<div class="ui three stackable cards">
{{range .Services}}
<a class="ui card gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
- {{svg (Printf "gitea-%s" .Name) 184}}
+ {{svg (printf "gitea-%s" .Name) 184}}
<div class="content">
<div class="header gt-tc">
{{.Title}}
</div>
<div class="description gt-tc">
- {{(Printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
+ {{(printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
</div>
</div>
</a>