gitea
Зеркало из https://github.com/go-gitea/gitea
1// Copyright 2018 The Gitea Authors. All rights reserved.
2// Copyright 2014 The Gogs Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package templates
6
7import (
8"fmt"
9"html"
10"html/template"
11"net/url"
12"reflect"
13"slices"
14"strings"
15"time"
16
17user_model "code.gitea.io/gitea/models/user"
18"code.gitea.io/gitea/modules/base"
19"code.gitea.io/gitea/modules/markup"
20"code.gitea.io/gitea/modules/setting"
21"code.gitea.io/gitea/modules/svg"
22"code.gitea.io/gitea/modules/templates/eval"
23"code.gitea.io/gitea/modules/timeutil"
24"code.gitea.io/gitea/modules/util"
25"code.gitea.io/gitea/services/gitdiff"
26"code.gitea.io/gitea/services/webtheme"
27)
28
29// NewFuncMap returns functions for injecting to templates
30func NewFuncMap() template.FuncMap {
31return map[string]any{
32"ctx": func() any { return nil }, // template context function
33
34"DumpVar": dumpVar,
35
36// -----------------------------------------------------------------
37// html/template related functions
38"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
39"Iif": iif,
40"Eval": evalTokens,
41"SafeHTML": safeHTML,
42"HTMLFormat": HTMLFormat,
43"HTMLEscape": htmlEscape,
44"QueryEscape": queryEscape,
45"JSEscape": jsEscapeSafe,
46"SanitizeHTML": SanitizeHTML,
47"URLJoin": util.URLJoin,
48"DotEscape": dotEscape,
49
50"PathEscape": url.PathEscape,
51"PathEscapeSegments": util.PathEscapeSegments,
52
53// utils
54"StringUtils": NewStringUtils,
55"SliceUtils": NewSliceUtils,
56"JsonUtils": NewJsonUtils,
57
58// -----------------------------------------------------------------
59// svg / avatar / icon / color
60"svg": svg.RenderHTML,
61"EntryIcon": base.EntryIcon,
62"MigrationIcon": migrationIcon,
63"ActionIcon": actionIcon,
64"SortArrow": sortArrow,
65"ContrastColor": util.ContrastColor,
66
67// -----------------------------------------------------------------
68// time / number / format
69"FileSize": base.FileSize,
70"CountFmt": base.FormatNumberSI,
71"TimeSince": timeutil.TimeSince,
72"TimeSinceUnix": timeutil.TimeSinceUnix,
73"DateTime": timeutil.DateTime,
74"Sec2Time": util.SecToTime,
75"LoadTimes": func(startTime time.Time) string {
76return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
77},
78
79// -----------------------------------------------------------------
80// setting
81"AppName": func() string {
82return setting.AppName
83},
84"AppSubUrl": func() string {
85return setting.AppSubURL
86},
87"AssetUrlPrefix": func() string {
88return setting.StaticURLPrefix + "/assets"
89},
90"AppUrl": func() string {
91// The usage of AppUrl should be avoided as much as possible,
92// because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect.
93// And it's difficult for Gitea to guess absolute URL correctly with zero configuration,
94// because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea.
95return setting.AppURL
96},
97"AppVer": func() string {
98return setting.AppVer
99},
100"AppDomain": func() string { // documented in mail-templates.md
101return setting.Domain
102},
103"AssetVersion": func() string {
104return setting.AssetVersion
105},
106"DefaultShowFullName": func() bool {
107return setting.UI.DefaultShowFullName
108},
109"ShowFooterTemplateLoadTime": func() bool {
110return setting.Other.ShowFooterTemplateLoadTime
111},
112"ShowFooterPoweredBy": func() bool {
113return setting.Other.ShowFooterPoweredBy
114},
115"AllowedReactions": func() []string {
116return setting.UI.Reactions
117},
118"CustomEmojis": func() map[string]string {
119return setting.UI.CustomEmojisMap
120},
121"MetaAuthor": func() string {
122return setting.UI.Meta.Author
123},
124"MetaDescription": func() string {
125return setting.UI.Meta.Description
126},
127"MetaKeywords": func() string {
128return setting.UI.Meta.Keywords
129},
130"EnableTimetracking": func() bool {
131return setting.Service.EnableTimetracking
132},
133"DisableGitHooks": func() bool {
134return setting.DisableGitHooks
135},
136"DisableWebhooks": func() bool {
137return setting.DisableWebhooks
138},
139"DisableImportLocal": func() bool {
140return !setting.ImportLocalPaths
141},
142"UserThemeName": userThemeName,
143"NotificationSettings": func() map[string]any {
144return map[string]any{
145"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
146"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
147"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
148"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
149}
150},
151"MermaidMaxSourceCharacters": func() int {
152return setting.MermaidMaxSourceCharacters
153},
154
155// -----------------------------------------------------------------
156// render
157"RenderCommitMessage": RenderCommitMessage,
158"RenderCommitMessageLinkSubject": renderCommitMessageLinkSubject,
159
160"RenderCommitBody": renderCommitBody,
161"RenderCodeBlock": renderCodeBlock,
162"RenderIssueTitle": renderIssueTitle,
163"RenderEmoji": renderEmoji,
164"ReactionToEmoji": reactionToEmoji,
165
166"RenderMarkdownToHtml": RenderMarkdownToHtml,
167"RenderLabel": renderLabel,
168"RenderLabels": RenderLabels,
169
170// -----------------------------------------------------------------
171// misc
172"ShortSha": base.ShortSha,
173"ActionContent2Commits": ActionContent2Commits,
174"IsMultilineCommitMessage": isMultilineCommitMessage,
175"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
176"MirrorRemoteAddress": mirrorRemoteAddress,
177
178"FilenameIsImage": filenameIsImage,
179"TabSizeClass": tabSizeClass,
180}
181}
182
183func HTMLFormat(s string, rawArgs ...any) template.HTML {
184args := slices.Clone(rawArgs)
185for i, v := range args {
186switch v := v.(type) {
187case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
188// for most basic types (including template.HTML which is safe), just do nothing and use it
189case string:
190args[i] = template.HTMLEscapeString(v)
191case fmt.Stringer:
192args[i] = template.HTMLEscapeString(v.String())
193default:
194args[i] = template.HTMLEscapeString(fmt.Sprint(v))
195}
196}
197return template.HTML(fmt.Sprintf(s, args...))
198}
199
200// safeHTML render raw as HTML
201func safeHTML(s any) template.HTML {
202switch v := s.(type) {
203case string:
204return template.HTML(v)
205case template.HTML:
206return v
207}
208panic(fmt.Sprintf("unexpected type %T", s))
209}
210
211// SanitizeHTML sanitizes the input by pre-defined markdown rules
212func SanitizeHTML(s string) template.HTML {
213return template.HTML(markup.Sanitize(s))
214}
215
216func htmlEscape(s any) template.HTML {
217switch v := s.(type) {
218case string:
219return template.HTML(html.EscapeString(v))
220case template.HTML:
221return v
222}
223panic(fmt.Sprintf("unexpected type %T", s))
224}
225
226func jsEscapeSafe(s string) template.HTML {
227return template.HTML(template.JSEscapeString(s))
228}
229
230func queryEscape(s string) template.URL {
231return template.URL(url.QueryEscape(s))
232}
233
234// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls
235func dotEscape(raw string) string {
236return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
237}
238
239// iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
240// and it could be simply used as "{{iif expr trueVal}}" (omit the falseVal).
241func iif(condition any, vals ...any) any {
242if isTemplateTruthy(condition) {
243return vals[0]
244} else if len(vals) > 1 {
245return vals[1]
246}
247return nil
248}
249
250func isTemplateTruthy(v any) bool {
251if v == nil {
252return false
253}
254
255rv := reflect.ValueOf(v)
256switch rv.Kind() {
257case reflect.Bool:
258return rv.Bool()
259case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
260return rv.Int() != 0
261case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
262return rv.Uint() != 0
263case reflect.Float32, reflect.Float64:
264return rv.Float() != 0
265case reflect.Complex64, reflect.Complex128:
266return rv.Complex() != 0
267case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
268return rv.Len() > 0
269case reflect.Struct:
270return true
271default:
272return !rv.IsNil()
273}
274}
275
276// evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details.
277// To use this helper function in templates, pass each token as a separate parameter.
278//
279// {{ $int64 := Eval $var "+" 1 }}
280// {{ $float64 := Eval $var "+" 1.0 }}
281//
282// Golang's template supports comparable int types, so the int64 result can be used in later statements like {{if lt $int64 10}}
283func evalTokens(tokens ...any) (any, error) {
284n, err := eval.Expr(tokens...)
285return n.Value, err
286}
287
288func userThemeName(user *user_model.User) string {
289if user == nil || user.Theme == "" {
290return setting.UI.DefaultTheme
291}
292if webtheme.IsThemeAvailable(user.Theme) {
293return user.Theme
294}
295return setting.UI.DefaultTheme
296}
297