code.gitea.io/gitea@v1.22.3/modules/templates/helper.go (about) 1 // Copyright 2018 The Gitea Authors. All rights reserved. 2 // Copyright 2014 The Gogs Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package templates 6 7 import ( 8 "fmt" 9 "html" 10 "html/template" 11 "net/url" 12 "reflect" 13 "slices" 14 "strings" 15 "time" 16 17 user_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 30 func NewFuncMap() template.FuncMap { 31 return 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": Eval, 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 { 76 return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" 77 }, 78 79 // ----------------------------------------------------------------- 80 // setting 81 "AppName": func() string { 82 return setting.AppName 83 }, 84 "AppSubUrl": func() string { 85 return setting.AppSubURL 86 }, 87 "AssetUrlPrefix": func() string { 88 return 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. 95 return setting.AppURL 96 }, 97 "AppVer": func() string { 98 return setting.AppVer 99 }, 100 "AppDomain": func() string { // documented in mail-templates.md 101 return setting.Domain 102 }, 103 "AssetVersion": func() string { 104 return setting.AssetVersion 105 }, 106 "DefaultShowFullName": func() bool { 107 return setting.UI.DefaultShowFullName 108 }, 109 "ShowFooterTemplateLoadTime": func() bool { 110 return setting.Other.ShowFooterTemplateLoadTime 111 }, 112 "ShowFooterPoweredBy": func() bool { 113 return setting.Other.ShowFooterPoweredBy 114 }, 115 "AllowedReactions": func() []string { 116 return setting.UI.Reactions 117 }, 118 "CustomEmojis": func() map[string]string { 119 return setting.UI.CustomEmojisMap 120 }, 121 "MetaAuthor": func() string { 122 return setting.UI.Meta.Author 123 }, 124 "MetaDescription": func() string { 125 return setting.UI.Meta.Description 126 }, 127 "MetaKeywords": func() string { 128 return setting.UI.Meta.Keywords 129 }, 130 "EnableTimetracking": func() bool { 131 return setting.Service.EnableTimetracking 132 }, 133 "DisableGitHooks": func() bool { 134 return setting.DisableGitHooks 135 }, 136 "DisableWebhooks": func() bool { 137 return setting.DisableWebhooks 138 }, 139 "DisableImportLocal": func() bool { 140 return !setting.ImportLocalPaths 141 }, 142 "UserThemeName": UserThemeName, 143 "NotificationSettings": func() map[string]any { 144 return 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 { 152 return 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 183 func HTMLFormat(s string, rawArgs ...any) template.HTML { 184 args := slices.Clone(rawArgs) 185 for i, v := range args { 186 switch v := v.(type) { 187 case 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 189 case string: 190 args[i] = template.HTMLEscapeString(v) 191 case fmt.Stringer: 192 args[i] = template.HTMLEscapeString(v.String()) 193 default: 194 args[i] = template.HTMLEscapeString(fmt.Sprint(v)) 195 } 196 } 197 return template.HTML(fmt.Sprintf(s, args...)) 198 } 199 200 // SafeHTML render raw as HTML 201 func SafeHTML(s any) template.HTML { 202 switch v := s.(type) { 203 case string: 204 return template.HTML(v) 205 case template.HTML: 206 return v 207 } 208 panic(fmt.Sprintf("unexpected type %T", s)) 209 } 210 211 // SanitizeHTML sanitizes the input by pre-defined markdown rules 212 func SanitizeHTML(s string) template.HTML { 213 return template.HTML(markup.Sanitize(s)) 214 } 215 216 func HTMLEscape(s any) template.HTML { 217 switch v := s.(type) { 218 case string: 219 return template.HTML(html.EscapeString(v)) 220 case template.HTML: 221 return v 222 } 223 panic(fmt.Sprintf("unexpected type %T", s)) 224 } 225 226 func JSEscapeSafe(s string) template.HTML { 227 return template.HTML(template.JSEscapeString(s)) 228 } 229 230 func QueryEscape(s string) template.URL { 231 return template.URL(url.QueryEscape(s)) 232 } 233 234 // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls 235 func DotEscape(raw string) string { 236 return 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). 241 func Iif(condition any, vals ...any) any { 242 if isTemplateTruthy(condition) { 243 return vals[0] 244 } else if len(vals) > 1 { 245 return vals[1] 246 } 247 return nil 248 } 249 250 func isTemplateTruthy(v any) bool { 251 if v == nil { 252 return false 253 } 254 255 rv := reflect.ValueOf(v) 256 switch rv.Kind() { 257 case reflect.Bool: 258 return rv.Bool() 259 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 260 return rv.Int() != 0 261 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 262 return rv.Uint() != 0 263 case reflect.Float32, reflect.Float64: 264 return rv.Float() != 0 265 case reflect.Complex64, reflect.Complex128: 266 return rv.Complex() != 0 267 case reflect.String, reflect.Slice, reflect.Array, reflect.Map: 268 return rv.Len() > 0 269 case reflect.Struct: 270 return true 271 default: 272 return !rv.IsNil() 273 } 274 } 275 276 // Eval the expression and return 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}} 283 func Eval(tokens ...any) (any, error) { 284 n, err := eval.Expr(tokens...) 285 return n.Value, err 286 } 287 288 func UserThemeName(user *user_model.User) string { 289 if user == nil || user.Theme == "" { 290 return setting.UI.DefaultTheme 291 } 292 if webtheme.IsThemeAvailable(user.Theme) { 293 return user.Theme 294 } 295 return setting.UI.DefaultTheme 296 }