code.gitea.io/gitea@v1.19.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  	"bytes"
     9  	"context"
    10  	"encoding/hex"
    11  	"errors"
    12  	"fmt"
    13  	"html"
    14  	"html/template"
    15  	"math"
    16  	"mime"
    17  	"net/url"
    18  	"path/filepath"
    19  	"reflect"
    20  	"regexp"
    21  	"runtime"
    22  	"strconv"
    23  	"strings"
    24  	texttmpl "text/template"
    25  	"time"
    26  	"unicode"
    27  
    28  	activities_model "code.gitea.io/gitea/models/activities"
    29  	"code.gitea.io/gitea/models/avatars"
    30  	issues_model "code.gitea.io/gitea/models/issues"
    31  	"code.gitea.io/gitea/models/organization"
    32  	repo_model "code.gitea.io/gitea/models/repo"
    33  	system_model "code.gitea.io/gitea/models/system"
    34  	user_model "code.gitea.io/gitea/models/user"
    35  	"code.gitea.io/gitea/modules/base"
    36  	"code.gitea.io/gitea/modules/emoji"
    37  	"code.gitea.io/gitea/modules/git"
    38  	giturl "code.gitea.io/gitea/modules/git/url"
    39  	gitea_html "code.gitea.io/gitea/modules/html"
    40  	"code.gitea.io/gitea/modules/json"
    41  	"code.gitea.io/gitea/modules/log"
    42  	"code.gitea.io/gitea/modules/markup"
    43  	"code.gitea.io/gitea/modules/markup/markdown"
    44  	"code.gitea.io/gitea/modules/repository"
    45  	"code.gitea.io/gitea/modules/setting"
    46  	"code.gitea.io/gitea/modules/svg"
    47  	"code.gitea.io/gitea/modules/timeutil"
    48  	"code.gitea.io/gitea/modules/util"
    49  	"code.gitea.io/gitea/services/gitdiff"
    50  
    51  	"github.com/editorconfig/editorconfig-core-go/v2"
    52  )
    53  
    54  // Used from static.go && dynamic.go
    55  var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
    56  
    57  // NewFuncMap returns functions for injecting to templates
    58  func NewFuncMap() []template.FuncMap {
    59  	return []template.FuncMap{map[string]interface{}{
    60  		"GoVer": func() string {
    61  			return util.ToTitleCase(runtime.Version())
    62  		},
    63  		"UseHTTPS": func() bool {
    64  			return strings.HasPrefix(setting.AppURL, "https")
    65  		},
    66  		"AppName": func() string {
    67  			return setting.AppName
    68  		},
    69  		"AppSubUrl": func() string {
    70  			return setting.AppSubURL
    71  		},
    72  		"AssetUrlPrefix": func() string {
    73  			return setting.StaticURLPrefix + "/assets"
    74  		},
    75  		"AppUrl": func() string {
    76  			// The usage of AppUrl should be avoided as much as possible,
    77  			// because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect.
    78  			// And it's difficult for Gitea to guess absolute URL correctly with zero configuration,
    79  			// because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea.
    80  			return setting.AppURL
    81  		},
    82  		"AppVer": func() string {
    83  			return setting.AppVer
    84  		},
    85  		"AppBuiltWith": func() string {
    86  			return setting.AppBuiltWith
    87  		},
    88  		"AppDomain": func() string {
    89  			return setting.Domain
    90  		},
    91  		"AssetVersion": func() string {
    92  			return setting.AssetVersion
    93  		},
    94  		"DisableGravatar": func(ctx context.Context) bool {
    95  			return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
    96  		},
    97  		"DefaultShowFullName": func() bool {
    98  			return setting.UI.DefaultShowFullName
    99  		},
   100  		"ShowFooterTemplateLoadTime": func() bool {
   101  			return setting.ShowFooterTemplateLoadTime
   102  		},
   103  		"LoadTimes": func(startTime time.Time) string {
   104  			return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
   105  		},
   106  		"AllowedReactions": func() []string {
   107  			return setting.UI.Reactions
   108  		},
   109  		"CustomEmojis": func() map[string]string {
   110  			return setting.UI.CustomEmojisMap
   111  		},
   112  		"Safe":           Safe,
   113  		"SafeJS":         SafeJS,
   114  		"JSEscape":       JSEscape,
   115  		"Str2html":       Str2html,
   116  		"TimeSince":      timeutil.TimeSince,
   117  		"TimeSinceUnix":  timeutil.TimeSinceUnix,
   118  		"FileSize":       base.FileSize,
   119  		"PrettyNumber":   base.PrettyNumber,
   120  		"JsPrettyNumber": JsPrettyNumber,
   121  		"Subtract":       base.Subtract,
   122  		"EntryIcon":      base.EntryIcon,
   123  		"MigrationIcon":  MigrationIcon,
   124  		"Add": func(a ...int) int {
   125  			sum := 0
   126  			for _, val := range a {
   127  				sum += val
   128  			}
   129  			return sum
   130  		},
   131  		"Mul": func(a ...int) int {
   132  			sum := 1
   133  			for _, val := range a {
   134  				sum *= val
   135  			}
   136  			return sum
   137  		},
   138  		"ActionIcon": ActionIcon,
   139  		"DateFmtLong": func(t time.Time) string {
   140  			return t.Format(time.RFC1123Z)
   141  		},
   142  		"DateFmtShort": func(t time.Time) string {
   143  			return t.Format("Jan 02, 2006")
   144  		},
   145  		"CountFmt": base.FormatNumberSI,
   146  		"SubStr": func(str string, start, length int) string {
   147  			if len(str) == 0 {
   148  				return ""
   149  			}
   150  			end := start + length
   151  			if length == -1 {
   152  				end = len(str)
   153  			}
   154  			if len(str) < end {
   155  				return str
   156  			}
   157  			return str[start:end]
   158  		},
   159  		"EllipsisString":                 base.EllipsisString,
   160  		"DiffTypeToStr":                  DiffTypeToStr,
   161  		"DiffLineTypeToStr":              DiffLineTypeToStr,
   162  		"ShortSha":                       base.ShortSha,
   163  		"ActionContent2Commits":          ActionContent2Commits,
   164  		"PathEscape":                     url.PathEscape,
   165  		"PathEscapeSegments":             util.PathEscapeSegments,
   166  		"URLJoin":                        util.URLJoin,
   167  		"RenderCommitMessage":            RenderCommitMessage,
   168  		"RenderCommitMessageLink":        RenderCommitMessageLink,
   169  		"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
   170  		"RenderCommitBody":               RenderCommitBody,
   171  		"RenderCodeBlock":                RenderCodeBlock,
   172  		"RenderIssueTitle":               RenderIssueTitle,
   173  		"RenderEmoji":                    RenderEmoji,
   174  		"RenderEmojiPlain":               emoji.ReplaceAliases,
   175  		"ReactionToEmoji":                ReactionToEmoji,
   176  		"RenderNote":                     RenderNote,
   177  		"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
   178  			output, err := markdown.RenderString(&markup.RenderContext{
   179  				Ctx:       ctx,
   180  				URLPrefix: setting.AppSubURL,
   181  			}, input)
   182  			if err != nil {
   183  				log.Error("RenderString: %v", err)
   184  			}
   185  			return template.HTML(output)
   186  		},
   187  		"IsMultilineCommitMessage": IsMultilineCommitMessage,
   188  		"ThemeColorMetaTag": func() string {
   189  			return setting.UI.ThemeColorMetaTag
   190  		},
   191  		"MetaAuthor": func() string {
   192  			return setting.UI.Meta.Author
   193  		},
   194  		"MetaDescription": func() string {
   195  			return setting.UI.Meta.Description
   196  		},
   197  		"MetaKeywords": func() string {
   198  			return setting.UI.Meta.Keywords
   199  		},
   200  		"UseServiceWorker": func() bool {
   201  			return setting.UI.UseServiceWorker
   202  		},
   203  		"EnableTimetracking": func() bool {
   204  			return setting.Service.EnableTimetracking
   205  		},
   206  		"FilenameIsImage": func(filename string) bool {
   207  			mimeType := mime.TypeByExtension(filepath.Ext(filename))
   208  			return strings.HasPrefix(mimeType, "image/")
   209  		},
   210  		"TabSizeClass": func(ec interface{}, filename string) string {
   211  			var (
   212  				value *editorconfig.Editorconfig
   213  				ok    bool
   214  			)
   215  			if ec != nil {
   216  				if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
   217  					return "tab-size-8"
   218  				}
   219  				def, err := value.GetDefinitionForFilename(filename)
   220  				if err != nil {
   221  					log.Error("tab size class: getting definition for filename: %v", err)
   222  					return "tab-size-8"
   223  				}
   224  				if def.TabWidth > 0 {
   225  					return fmt.Sprintf("tab-size-%d", def.TabWidth)
   226  				}
   227  			}
   228  			return "tab-size-8"
   229  		},
   230  		"SubJumpablePath": func(str string) []string {
   231  			var path []string
   232  			index := strings.LastIndex(str, "/")
   233  			if index != -1 && index != len(str) {
   234  				path = append(path, str[0:index+1], str[index+1:])
   235  			} else {
   236  				path = append(path, str)
   237  			}
   238  			return path
   239  		},
   240  		"DiffStatsWidth": func(adds, dels int) string {
   241  			return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100)
   242  		},
   243  		"Json": func(in interface{}) string {
   244  			out, err := json.Marshal(in)
   245  			if err != nil {
   246  				return ""
   247  			}
   248  			return string(out)
   249  		},
   250  		"JsonPrettyPrint": func(in string) string {
   251  			var out bytes.Buffer
   252  			err := json.Indent(&out, []byte(in), "", "  ")
   253  			if err != nil {
   254  				return ""
   255  			}
   256  			return out.String()
   257  		},
   258  		"DisableGitHooks": func() bool {
   259  			return setting.DisableGitHooks
   260  		},
   261  		"DisableWebhooks": func() bool {
   262  			return setting.DisableWebhooks
   263  		},
   264  		"DisableImportLocal": func() bool {
   265  			return !setting.ImportLocalPaths
   266  		},
   267  		"Dict": func(values ...interface{}) (map[string]interface{}, error) {
   268  			if len(values)%2 != 0 {
   269  				return nil, errors.New("invalid dict call")
   270  			}
   271  			dict := make(map[string]interface{}, len(values)/2)
   272  			for i := 0; i < len(values); i += 2 {
   273  				key, ok := values[i].(string)
   274  				if !ok {
   275  					return nil, errors.New("dict keys must be strings")
   276  				}
   277  				dict[key] = values[i+1]
   278  			}
   279  			return dict, nil
   280  		},
   281  		"Printf":   fmt.Sprintf,
   282  		"Escape":   Escape,
   283  		"Sec2Time": util.SecToTime,
   284  		"ParseDeadline": func(deadline string) []string {
   285  			return strings.Split(deadline, "|")
   286  		},
   287  		"DefaultTheme": func() string {
   288  			return setting.UI.DefaultTheme
   289  		},
   290  		// pass key-value pairs to a partial template which receives them as a dict
   291  		"dict": func(values ...interface{}) (map[string]interface{}, error) {
   292  			if len(values) == 0 {
   293  				return nil, errors.New("invalid dict call")
   294  			}
   295  
   296  			dict := make(map[string]interface{})
   297  			return util.MergeInto(dict, values...)
   298  		},
   299  		/* like dict but merge key-value pairs into the first dict and return it */
   300  		"mergeinto": func(root map[string]interface{}, values ...interface{}) (map[string]interface{}, error) {
   301  			if len(values) == 0 {
   302  				return nil, errors.New("invalid mergeinto call")
   303  			}
   304  
   305  			dict := make(map[string]interface{})
   306  			for key, value := range root {
   307  				dict[key] = value
   308  			}
   309  
   310  			return util.MergeInto(dict, values...)
   311  		},
   312  		"percentage": func(n int, values ...int) float32 {
   313  			sum := 0
   314  			for i := 0; i < len(values); i++ {
   315  				sum += values[i]
   316  			}
   317  			return float32(n) * 100 / float32(sum)
   318  		},
   319  		"CommentMustAsDiff":   gitdiff.CommentMustAsDiff,
   320  		"MirrorRemoteAddress": mirrorRemoteAddress,
   321  		"NotificationSettings": func() map[string]interface{} {
   322  			return map[string]interface{}{
   323  				"MinTimeout":            int(setting.UI.Notification.MinTimeout / time.Millisecond),
   324  				"TimeoutStep":           int(setting.UI.Notification.TimeoutStep / time.Millisecond),
   325  				"MaxTimeout":            int(setting.UI.Notification.MaxTimeout / time.Millisecond),
   326  				"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
   327  			}
   328  		},
   329  		"containGeneric": func(arr, v interface{}) bool {
   330  			arrV := reflect.ValueOf(arr)
   331  			if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
   332  				return strings.Contains(arr.(string), v.(string))
   333  			}
   334  
   335  			if arrV.Kind() == reflect.Slice {
   336  				for i := 0; i < arrV.Len(); i++ {
   337  					iV := arrV.Index(i)
   338  					if !iV.CanInterface() {
   339  						continue
   340  					}
   341  					if iV.Interface() == v {
   342  						return true
   343  					}
   344  				}
   345  			}
   346  
   347  			return false
   348  		},
   349  		"contain": func(s []int64, id int64) bool {
   350  			for i := 0; i < len(s); i++ {
   351  				if s[i] == id {
   352  					return true
   353  				}
   354  			}
   355  			return false
   356  		},
   357  		"svg":            svg.RenderHTML,
   358  		"avatar":         Avatar,
   359  		"avatarHTML":     AvatarHTML,
   360  		"avatarByAction": AvatarByAction,
   361  		"avatarByEmail":  AvatarByEmail,
   362  		"repoAvatar":     RepoAvatar,
   363  		"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
   364  			// if needed
   365  			if len(normSort) == 0 || len(urlSort) == 0 {
   366  				return ""
   367  			}
   368  
   369  			if len(urlSort) == 0 && isDefault {
   370  				// if sort is sorted as default add arrow tho this table header
   371  				if isDefault {
   372  					return svg.RenderHTML("octicon-triangle-down", 16)
   373  				}
   374  			} else {
   375  				// if sort arg is in url test if it correlates with column header sort arguments
   376  				// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
   377  				if urlSort == normSort {
   378  					// the table is sorted with this header normal
   379  					return svg.RenderHTML("octicon-triangle-up", 16)
   380  				} else if urlSort == revSort {
   381  					// the table is sorted with this header reverse
   382  					return svg.RenderHTML("octicon-triangle-down", 16)
   383  				}
   384  			}
   385  			// the table is NOT sorted with this header
   386  			return ""
   387  		},
   388  		"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
   389  			return template.HTML(RenderLabel(ctx, label))
   390  		},
   391  		"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
   392  			htmlCode := `<span class="labels-list">`
   393  			for _, label := range labels {
   394  				// Protect against nil value in labels - shouldn't happen but would cause a panic if so
   395  				if label == nil {
   396  					continue
   397  				}
   398  				htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
   399  					repoLink, label.ID, RenderLabel(ctx, label))
   400  			}
   401  			htmlCode += "</span>"
   402  			return template.HTML(htmlCode)
   403  		},
   404  		"MermaidMaxSourceCharacters": func() int {
   405  			return setting.MermaidMaxSourceCharacters
   406  		},
   407  		"Join":        strings.Join,
   408  		"QueryEscape": url.QueryEscape,
   409  		"DotEscape":   DotEscape,
   410  		"Iterate": func(arg interface{}) (items []uint64) {
   411  			count := uint64(0)
   412  			switch val := arg.(type) {
   413  			case uint64:
   414  				count = val
   415  			case *uint64:
   416  				count = *val
   417  			case int64:
   418  				if val < 0 {
   419  					val = 0
   420  				}
   421  				count = uint64(val)
   422  			case *int64:
   423  				if *val < 0 {
   424  					*val = 0
   425  				}
   426  				count = uint64(*val)
   427  			case int:
   428  				if val < 0 {
   429  					val = 0
   430  				}
   431  				count = uint64(val)
   432  			case *int:
   433  				if *val < 0 {
   434  					*val = 0
   435  				}
   436  				count = uint64(*val)
   437  			case uint:
   438  				count = uint64(val)
   439  			case *uint:
   440  				count = uint64(*val)
   441  			case int32:
   442  				if val < 0 {
   443  					val = 0
   444  				}
   445  				count = uint64(val)
   446  			case *int32:
   447  				if *val < 0 {
   448  					*val = 0
   449  				}
   450  				count = uint64(*val)
   451  			case uint32:
   452  				count = uint64(val)
   453  			case *uint32:
   454  				count = uint64(*val)
   455  			case string:
   456  				cnt, _ := strconv.ParseInt(val, 10, 64)
   457  				if cnt < 0 {
   458  					cnt = 0
   459  				}
   460  				count = uint64(cnt)
   461  			}
   462  			if count <= 0 {
   463  				return items
   464  			}
   465  			for i := uint64(0); i < count; i++ {
   466  				items = append(items, i)
   467  			}
   468  			return items
   469  		},
   470  		"HasPrefix": strings.HasPrefix,
   471  		"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
   472  			var curBranch string
   473  			if repo.ID != baseRepo.ID {
   474  				curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
   475  			}
   476  			curBranch += util.PathEscapeSegments(branchName)
   477  
   478  			return fmt.Sprintf("%s/compare/%s...%s",
   479  				baseRepo.Link(),
   480  				util.PathEscapeSegments(baseRepo.DefaultBranch),
   481  				curBranch,
   482  			)
   483  		},
   484  		"RefShortName": func(ref string) string {
   485  			return git.RefName(ref).ShortName()
   486  		},
   487  	}}
   488  }
   489  
   490  // NewTextFuncMap returns functions for injecting to text templates
   491  // It's a subset of those used for HTML and other templates
   492  func NewTextFuncMap() []texttmpl.FuncMap {
   493  	return []texttmpl.FuncMap{map[string]interface{}{
   494  		"GoVer": func() string {
   495  			return util.ToTitleCase(runtime.Version())
   496  		},
   497  		"AppName": func() string {
   498  			return setting.AppName
   499  		},
   500  		"AppSubUrl": func() string {
   501  			return setting.AppSubURL
   502  		},
   503  		"AppUrl": func() string {
   504  			return setting.AppURL
   505  		},
   506  		"AppVer": func() string {
   507  			return setting.AppVer
   508  		},
   509  		"AppBuiltWith": func() string {
   510  			return setting.AppBuiltWith
   511  		},
   512  		"AppDomain": func() string {
   513  			return setting.Domain
   514  		},
   515  		"TimeSince":     timeutil.TimeSince,
   516  		"TimeSinceUnix": timeutil.TimeSinceUnix,
   517  		"DateFmtLong": func(t time.Time) string {
   518  			return t.Format(time.RFC1123Z)
   519  		},
   520  		"DateFmtShort": func(t time.Time) string {
   521  			return t.Format("Jan 02, 2006")
   522  		},
   523  		"SubStr": func(str string, start, length int) string {
   524  			if len(str) == 0 {
   525  				return ""
   526  			}
   527  			end := start + length
   528  			if length == -1 {
   529  				end = len(str)
   530  			}
   531  			if len(str) < end {
   532  				return str
   533  			}
   534  			return str[start:end]
   535  		},
   536  		"EllipsisString": base.EllipsisString,
   537  		"URLJoin":        util.URLJoin,
   538  		"Dict": func(values ...interface{}) (map[string]interface{}, error) {
   539  			if len(values)%2 != 0 {
   540  				return nil, errors.New("invalid dict call")
   541  			}
   542  			dict := make(map[string]interface{}, len(values)/2)
   543  			for i := 0; i < len(values); i += 2 {
   544  				key, ok := values[i].(string)
   545  				if !ok {
   546  					return nil, errors.New("dict keys must be strings")
   547  				}
   548  				dict[key] = values[i+1]
   549  			}
   550  			return dict, nil
   551  		},
   552  		"Printf":   fmt.Sprintf,
   553  		"Escape":   Escape,
   554  		"Sec2Time": util.SecToTime,
   555  		"ParseDeadline": func(deadline string) []string {
   556  			return strings.Split(deadline, "|")
   557  		},
   558  		"dict": func(values ...interface{}) (map[string]interface{}, error) {
   559  			if len(values) == 0 {
   560  				return nil, errors.New("invalid dict call")
   561  			}
   562  
   563  			dict := make(map[string]interface{})
   564  
   565  			for i := 0; i < len(values); i++ {
   566  				switch key := values[i].(type) {
   567  				case string:
   568  					i++
   569  					if i == len(values) {
   570  						return nil, errors.New("specify the key for non array values")
   571  					}
   572  					dict[key] = values[i]
   573  				case map[string]interface{}:
   574  					m := values[i].(map[string]interface{})
   575  					for i, v := range m {
   576  						dict[i] = v
   577  					}
   578  				default:
   579  					return nil, errors.New("dict values must be maps")
   580  				}
   581  			}
   582  			return dict, nil
   583  		},
   584  		"percentage": func(n int, values ...int) float32 {
   585  			sum := 0
   586  			for i := 0; i < len(values); i++ {
   587  				sum += values[i]
   588  			}
   589  			return float32(n) * 100 / float32(sum)
   590  		},
   591  		"Add": func(a ...int) int {
   592  			sum := 0
   593  			for _, val := range a {
   594  				sum += val
   595  			}
   596  			return sum
   597  		},
   598  		"Mul": func(a ...int) int {
   599  			sum := 1
   600  			for _, val := range a {
   601  				sum *= val
   602  			}
   603  			return sum
   604  		},
   605  		"QueryEscape": url.QueryEscape,
   606  	}}
   607  }
   608  
   609  // AvatarHTML creates the HTML for an avatar
   610  func AvatarHTML(src string, size int, class, name string) template.HTML {
   611  	sizeStr := fmt.Sprintf(`%d`, size)
   612  
   613  	if name == "" {
   614  		name = "avatar"
   615  	}
   616  
   617  	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
   618  }
   619  
   620  // Avatar renders user avatars. args: user, size (int), class (string)
   621  func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
   622  	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
   623  
   624  	switch t := item.(type) {
   625  	case *user_model.User:
   626  		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
   627  		if src != "" {
   628  			return AvatarHTML(src, size, class, t.DisplayName())
   629  		}
   630  	case *repo_model.Collaborator:
   631  		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
   632  		if src != "" {
   633  			return AvatarHTML(src, size, class, t.DisplayName())
   634  		}
   635  	case *organization.Organization:
   636  		src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
   637  		if src != "" {
   638  			return AvatarHTML(src, size, class, t.AsUser().DisplayName())
   639  		}
   640  	}
   641  
   642  	return template.HTML("")
   643  }
   644  
   645  // AvatarByAction renders user avatars from action. args: action, size (int), class (string)
   646  func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
   647  	action.LoadActUser(ctx)
   648  	return Avatar(ctx, action.ActUser, others...)
   649  }
   650  
   651  // RepoAvatar renders repo avatars. args: repo, size(int), class (string)
   652  func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
   653  	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
   654  
   655  	src := repo.RelAvatarLink()
   656  	if src != "" {
   657  		return AvatarHTML(src, size, class, repo.FullName())
   658  	}
   659  	return template.HTML("")
   660  }
   661  
   662  // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
   663  func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
   664  	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
   665  	src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
   666  
   667  	if src != "" {
   668  		return AvatarHTML(src, size, class, name)
   669  	}
   670  
   671  	return template.HTML("")
   672  }
   673  
   674  // Safe render raw as HTML
   675  func Safe(raw string) template.HTML {
   676  	return template.HTML(raw)
   677  }
   678  
   679  // SafeJS renders raw as JS
   680  func SafeJS(raw string) template.JS {
   681  	return template.JS(raw)
   682  }
   683  
   684  // Str2html render Markdown text to HTML
   685  func Str2html(raw string) template.HTML {
   686  	return template.HTML(markup.Sanitize(raw))
   687  }
   688  
   689  // Escape escapes a HTML string
   690  func Escape(raw string) string {
   691  	return html.EscapeString(raw)
   692  }
   693  
   694  // JSEscape escapes a JS string
   695  func JSEscape(raw string) string {
   696  	return template.JSEscapeString(raw)
   697  }
   698  
   699  // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
   700  func DotEscape(raw string) string {
   701  	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
   702  }
   703  
   704  // RenderCommitMessage renders commit message with XSS-safe and special links.
   705  func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
   706  	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
   707  }
   708  
   709  // RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
   710  // default url, handling for special links.
   711  func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
   712  	cleanMsg := template.HTMLEscapeString(msg)
   713  	// we can safely assume that it will not return any error, since there
   714  	// shouldn't be any special HTML.
   715  	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
   716  		Ctx:         ctx,
   717  		URLPrefix:   urlPrefix,
   718  		DefaultLink: urlDefault,
   719  		Metas:       metas,
   720  	}, cleanMsg)
   721  	if err != nil {
   722  		log.Error("RenderCommitMessage: %v", err)
   723  		return ""
   724  	}
   725  	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
   726  	if len(msgLines) == 0 {
   727  		return template.HTML("")
   728  	}
   729  	return template.HTML(msgLines[0])
   730  }
   731  
   732  // RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
   733  // the provided default url, handling for special links without email to links.
   734  func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
   735  	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
   736  	lineEnd := strings.IndexByte(msgLine, '\n')
   737  	if lineEnd > 0 {
   738  		msgLine = msgLine[:lineEnd]
   739  	}
   740  	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
   741  	if len(msgLine) == 0 {
   742  		return template.HTML("")
   743  	}
   744  
   745  	// we can safely assume that it will not return any error, since there
   746  	// shouldn't be any special HTML.
   747  	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
   748  		Ctx:         ctx,
   749  		URLPrefix:   urlPrefix,
   750  		DefaultLink: urlDefault,
   751  		Metas:       metas,
   752  	}, template.HTMLEscapeString(msgLine))
   753  	if err != nil {
   754  		log.Error("RenderCommitMessageSubject: %v", err)
   755  		return template.HTML("")
   756  	}
   757  	return template.HTML(renderedMessage)
   758  }
   759  
   760  // RenderCommitBody extracts the body of a commit message without its title.
   761  func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
   762  	msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
   763  	lineEnd := strings.IndexByte(msgLine, '\n')
   764  	if lineEnd > 0 {
   765  		msgLine = msgLine[lineEnd+1:]
   766  	} else {
   767  		return template.HTML("")
   768  	}
   769  	msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
   770  	if len(msgLine) == 0 {
   771  		return template.HTML("")
   772  	}
   773  
   774  	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
   775  		Ctx:       ctx,
   776  		URLPrefix: urlPrefix,
   777  		Metas:     metas,
   778  	}, template.HTMLEscapeString(msgLine))
   779  	if err != nil {
   780  		log.Error("RenderCommitMessage: %v", err)
   781  		return ""
   782  	}
   783  	return template.HTML(renderedMessage)
   784  }
   785  
   786  // Match text that is between back ticks.
   787  var codeMatcher = regexp.MustCompile("`([^`]+)`")
   788  
   789  // RenderCodeBlock renders "`…`" as highlighted "<code>" block.
   790  // Intended for issue and PR titles, these containers should have styles for "<code>" elements
   791  func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
   792  	htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
   793  	return template.HTML(htmlWithCodeTags)
   794  }
   795  
   796  // RenderIssueTitle renders issue/pull title with defined post processors
   797  func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
   798  	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
   799  		Ctx:       ctx,
   800  		URLPrefix: urlPrefix,
   801  		Metas:     metas,
   802  	}, template.HTMLEscapeString(text))
   803  	if err != nil {
   804  		log.Error("RenderIssueTitle: %v", err)
   805  		return template.HTML("")
   806  	}
   807  	return template.HTML(renderedText)
   808  }
   809  
   810  // RenderLabel renders a label
   811  func RenderLabel(ctx context.Context, label *issues_model.Label) string {
   812  	labelScope := label.ExclusiveScope()
   813  
   814  	textColor := "#111"
   815  	if label.UseLightTextColor() {
   816  		textColor = "#eee"
   817  	}
   818  
   819  	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
   820  
   821  	if labelScope == "" {
   822  		// Regular label
   823  		return fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
   824  			textColor, label.Color, description, RenderEmoji(ctx, label.Name))
   825  	}
   826  
   827  	// Scoped label
   828  	scopeText := RenderEmoji(ctx, labelScope)
   829  	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
   830  
   831  	itemColor := label.Color
   832  	scopeColor := label.Color
   833  	if r, g, b, err := label.ColorRGB(); err == nil {
   834  		// Make scope and item background colors slightly darker and lighter respectively.
   835  		// More contrast needed with higher luminance, empirically tweaked.
   836  		luminance := (0.299*r + 0.587*g + 0.114*b) / 255
   837  		contrast := 0.01 + luminance*0.03
   838  		// Ensure we add the same amount of contrast also near 0 and 1.
   839  		darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
   840  		lighten := contrast + math.Max(contrast-luminance, 0.0)
   841  		// Compute factor to keep RGB values proportional.
   842  		darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
   843  		lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
   844  
   845  		scopeBytes := []byte{
   846  			uint8(math.Min(math.Round(r*darkenFactor), 255)),
   847  			uint8(math.Min(math.Round(g*darkenFactor), 255)),
   848  			uint8(math.Min(math.Round(b*darkenFactor), 255)),
   849  		}
   850  		itemBytes := []byte{
   851  			uint8(math.Min(math.Round(r*lightenFactor), 255)),
   852  			uint8(math.Min(math.Round(g*lightenFactor), 255)),
   853  			uint8(math.Min(math.Round(b*lightenFactor), 255)),
   854  		}
   855  
   856  		itemColor = "#" + hex.EncodeToString(itemBytes)
   857  		scopeColor = "#" + hex.EncodeToString(scopeBytes)
   858  	}
   859  
   860  	return fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
   861  		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
   862  		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
   863  		"</span>",
   864  		description,
   865  		textColor, scopeColor, scopeText,
   866  		textColor, itemColor, itemText)
   867  }
   868  
   869  // RenderEmoji renders html text with emoji post processors
   870  func RenderEmoji(ctx context.Context, text string) template.HTML {
   871  	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
   872  		template.HTMLEscapeString(text))
   873  	if err != nil {
   874  		log.Error("RenderEmoji: %v", err)
   875  		return template.HTML("")
   876  	}
   877  	return template.HTML(renderedText)
   878  }
   879  
   880  // ReactionToEmoji renders emoji for use in reactions
   881  func ReactionToEmoji(reaction string) template.HTML {
   882  	val := emoji.FromCode(reaction)
   883  	if val != nil {
   884  		return template.HTML(val.Emoji)
   885  	}
   886  	val = emoji.FromAlias(reaction)
   887  	if val != nil {
   888  		return template.HTML(val.Emoji)
   889  	}
   890  	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
   891  }
   892  
   893  // RenderNote renders the contents of a git-notes file as a commit message.
   894  func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
   895  	cleanMsg := template.HTMLEscapeString(msg)
   896  	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
   897  		Ctx:       ctx,
   898  		URLPrefix: urlPrefix,
   899  		Metas:     metas,
   900  	}, cleanMsg)
   901  	if err != nil {
   902  		log.Error("RenderNote: %v", err)
   903  		return ""
   904  	}
   905  	return template.HTML(fullMessage)
   906  }
   907  
   908  // IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
   909  func IsMultilineCommitMessage(msg string) bool {
   910  	return strings.Count(strings.TrimSpace(msg), "\n") >= 1
   911  }
   912  
   913  // Actioner describes an action
   914  type Actioner interface {
   915  	GetOpType() activities_model.ActionType
   916  	GetActUserName() string
   917  	GetRepoUserName() string
   918  	GetRepoName() string
   919  	GetRepoPath() string
   920  	GetRepoLink() string
   921  	GetBranch() string
   922  	GetContent() string
   923  	GetCreate() time.Time
   924  	GetIssueInfos() []string
   925  }
   926  
   927  // ActionIcon accepts an action operation type and returns an icon class name.
   928  func ActionIcon(opType activities_model.ActionType) string {
   929  	switch opType {
   930  	case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
   931  		return "repo"
   932  	case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
   933  		return "git-commit"
   934  	case activities_model.ActionCreateIssue:
   935  		return "issue-opened"
   936  	case activities_model.ActionCreatePullRequest:
   937  		return "git-pull-request"
   938  	case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
   939  		return "comment-discussion"
   940  	case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
   941  		return "git-merge"
   942  	case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
   943  		return "issue-closed"
   944  	case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
   945  		return "issue-reopened"
   946  	case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
   947  		return "mirror"
   948  	case activities_model.ActionApprovePullRequest:
   949  		return "check"
   950  	case activities_model.ActionRejectPullRequest:
   951  		return "diff"
   952  	case activities_model.ActionPublishRelease:
   953  		return "tag"
   954  	case activities_model.ActionPullReviewDismissed:
   955  		return "x"
   956  	default:
   957  		return "question"
   958  	}
   959  }
   960  
   961  // ActionContent2Commits converts action content to push commits
   962  func ActionContent2Commits(act Actioner) *repository.PushCommits {
   963  	push := repository.NewPushCommits()
   964  
   965  	if act == nil || act.GetContent() == "" {
   966  		return push
   967  	}
   968  
   969  	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
   970  		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
   971  	}
   972  
   973  	if push.Len == 0 {
   974  		push.Len = len(push.Commits)
   975  	}
   976  
   977  	return push
   978  }
   979  
   980  // DiffTypeToStr returns diff type name
   981  func DiffTypeToStr(diffType int) string {
   982  	diffTypes := map[int]string{
   983  		1: "add", 2: "modify", 3: "del", 4: "rename", 5: "copy",
   984  	}
   985  	return diffTypes[diffType]
   986  }
   987  
   988  // DiffLineTypeToStr returns diff line type name
   989  func DiffLineTypeToStr(diffType int) string {
   990  	switch diffType {
   991  	case 2:
   992  		return "add"
   993  	case 3:
   994  		return "del"
   995  	case 4:
   996  		return "tag"
   997  	}
   998  	return "same"
   999  }
  1000  
  1001  // MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
  1002  func MigrationIcon(hostname string) string {
  1003  	switch hostname {
  1004  	case "github.com":
  1005  		return "octicon-mark-github"
  1006  	default:
  1007  		return "gitea-git"
  1008  	}
  1009  }
  1010  
  1011  func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
  1012  	// Split template into subject and body
  1013  	var subjectContent []byte
  1014  	bodyContent := content
  1015  	loc := mailSubjectSplit.FindIndex(content)
  1016  	if loc != nil {
  1017  		subjectContent = content[0:loc[0]]
  1018  		bodyContent = content[loc[1]:]
  1019  	}
  1020  	if _, err := stpl.New(name).
  1021  		Parse(string(subjectContent)); err != nil {
  1022  		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
  1023  	}
  1024  	if _, err := btpl.New(name).
  1025  		Parse(string(bodyContent)); err != nil {
  1026  		log.Warn("Failed to parse template [%s/body]: %v", name, err)
  1027  	}
  1028  }
  1029  
  1030  type remoteAddress struct {
  1031  	Address  string
  1032  	Username string
  1033  	Password string
  1034  }
  1035  
  1036  func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
  1037  	a := remoteAddress{}
  1038  
  1039  	remoteURL := m.OriginalURL
  1040  	if ignoreOriginalURL || remoteURL == "" {
  1041  		var err error
  1042  		remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
  1043  		if err != nil {
  1044  			log.Error("GetRemoteURL %v", err)
  1045  			return a
  1046  		}
  1047  	}
  1048  
  1049  	u, err := giturl.Parse(remoteURL)
  1050  	if err != nil {
  1051  		log.Error("giturl.Parse %v", err)
  1052  		return a
  1053  	}
  1054  
  1055  	if u.Scheme != "ssh" && u.Scheme != "file" {
  1056  		if u.User != nil {
  1057  			a.Username = u.User.Username()
  1058  			a.Password, _ = u.User.Password()
  1059  		}
  1060  		u.User = nil
  1061  	}
  1062  	a.Address = u.String()
  1063  
  1064  	return a
  1065  }
  1066  
  1067  // JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent
  1068  // JS will replace the number with locale-specific separators, based on the user's selected language
  1069  func JsPrettyNumber(i interface{}) template.HTML {
  1070  	num := util.NumberIntoInt64(i)
  1071  
  1072  	return template.HTML(`<span class="js-pretty-number" data-value="` + strconv.FormatInt(num, 10) + `">` + base.PrettyNumber(num) + `</span>`)
  1073  }