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  }