1// Copyright 2018 The Gitea Authors. All rights reserved.
2// Copyright 2014 The Gogs Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package templates
7
8import (
9	"bytes"
10	"errors"
11	"fmt"
12	"html"
13	"html/template"
14	"mime"
15	"net/url"
16	"path/filepath"
17	"reflect"
18	"regexp"
19	"runtime"
20	"strings"
21	texttmpl "text/template"
22	"time"
23	"unicode"
24
25	"code.gitea.io/gitea/models"
26	"code.gitea.io/gitea/models/avatars"
27	repo_model "code.gitea.io/gitea/models/repo"
28	user_model "code.gitea.io/gitea/models/user"
29	"code.gitea.io/gitea/modules/base"
30	"code.gitea.io/gitea/modules/emoji"
31	"code.gitea.io/gitea/modules/git"
32	"code.gitea.io/gitea/modules/json"
33	"code.gitea.io/gitea/modules/log"
34	"code.gitea.io/gitea/modules/markup"
35	"code.gitea.io/gitea/modules/repository"
36	"code.gitea.io/gitea/modules/setting"
37	"code.gitea.io/gitea/modules/svg"
38	"code.gitea.io/gitea/modules/timeutil"
39	"code.gitea.io/gitea/modules/util"
40	"code.gitea.io/gitea/services/gitdiff"
41	"golang.org/x/text/cases"
42	"golang.org/x/text/language"
43
44	"github.com/editorconfig/editorconfig-core-go/v2"
45)
46
47// Used from static.go && dynamic.go
48var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
49
50// NewFuncMap returns functions for injecting to templates
51func NewFuncMap() []template.FuncMap {
52	return []template.FuncMap{map[string]interface{}{
53		"GoVer": func() string {
54			return cases.Title(language.English).String(runtime.Version())
55		},
56		"UseHTTPS": func() bool {
57			return strings.HasPrefix(setting.AppURL, "https")
58		},
59		"AppName": func() string {
60			return setting.AppName
61		},
62		"AppSubUrl": func() string {
63			return setting.AppSubURL
64		},
65		"AssetUrlPrefix": func() string {
66			return setting.StaticURLPrefix + "/assets"
67		},
68		"AppUrl": func() string {
69			return setting.AppURL
70		},
71		"AppVer": func() string {
72			return setting.AppVer
73		},
74		"AppBuiltWith": func() string {
75			return setting.AppBuiltWith
76		},
77		"AppDomain": func() string {
78			return setting.Domain
79		},
80		"DisableGravatar": func() bool {
81			return setting.DisableGravatar
82		},
83		"DefaultShowFullName": func() bool {
84			return setting.UI.DefaultShowFullName
85		},
86		"ShowFooterTemplateLoadTime": func() bool {
87			return setting.ShowFooterTemplateLoadTime
88		},
89		"LoadTimes": func(startTime time.Time) string {
90			return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
91		},
92		"AllowedReactions": func() []string {
93			return setting.UI.Reactions
94		},
95		"CustomEmojis": func() map[string]string {
96			return setting.UI.CustomEmojisMap
97		},
98		"Safe":          Safe,
99		"SafeJS":        SafeJS,
100		"JSEscape":      JSEscape,
101		"Str2html":      Str2html,
102		"TimeSince":     timeutil.TimeSince,
103		"TimeSinceUnix": timeutil.TimeSinceUnix,
104		"RawTimeSince":  timeutil.RawTimeSince,
105		"FileSize":      base.FileSize,
106		"PrettyNumber":  base.PrettyNumber,
107		"Subtract":      base.Subtract,
108		"EntryIcon":     base.EntryIcon,
109		"MigrationIcon": MigrationIcon,
110		"Add": func(a ...int) int {
111			sum := 0
112			for _, val := range a {
113				sum += val
114			}
115			return sum
116		},
117		"Mul": func(a ...int) int {
118			sum := 1
119			for _, val := range a {
120				sum *= val
121			}
122			return sum
123		},
124		"ActionIcon": ActionIcon,
125		"DateFmtLong": func(t time.Time) string {
126			return t.Format(time.RFC1123Z)
127		},
128		"DateFmtShort": func(t time.Time) string {
129			return t.Format("Jan 02, 2006")
130		},
131		"CountFmt": base.FormatNumberSI,
132		"SubStr": func(str string, start, length int) string {
133			if len(str) == 0 {
134				return ""
135			}
136			end := start + length
137			if length == -1 {
138				end = len(str)
139			}
140			if len(str) < end {
141				return str
142			}
143			return str[start:end]
144		},
145		"EllipsisString":                 base.EllipsisString,
146		"DiffTypeToStr":                  DiffTypeToStr,
147		"DiffLineTypeToStr":              DiffLineTypeToStr,
148		"Sha1":                           Sha1,
149		"ShortSha":                       base.ShortSha,
150		"MD5":                            base.EncodeMD5,
151		"ActionContent2Commits":          ActionContent2Commits,
152		"PathEscape":                     url.PathEscape,
153		"PathEscapeSegments":             util.PathEscapeSegments,
154		"URLJoin":                        util.URLJoin,
155		"RenderCommitMessage":            RenderCommitMessage,
156		"RenderCommitMessageLink":        RenderCommitMessageLink,
157		"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
158		"RenderCommitBody":               RenderCommitBody,
159		"RenderIssueTitle":               RenderIssueTitle,
160		"RenderEmoji":                    RenderEmoji,
161		"RenderEmojiPlain":               emoji.ReplaceAliases,
162		"ReactionToEmoji":                ReactionToEmoji,
163		"RenderNote":                     RenderNote,
164		"IsMultilineCommitMessage":       IsMultilineCommitMessage,
165		"ThemeColorMetaTag": func() string {
166			return setting.UI.ThemeColorMetaTag
167		},
168		"MetaAuthor": func() string {
169			return setting.UI.Meta.Author
170		},
171		"MetaDescription": func() string {
172			return setting.UI.Meta.Description
173		},
174		"MetaKeywords": func() string {
175			return setting.UI.Meta.Keywords
176		},
177		"UseServiceWorker": func() bool {
178			return setting.UI.UseServiceWorker
179		},
180		"EnableTimetracking": func() bool {
181			return setting.Service.EnableTimetracking
182		},
183		"FilenameIsImage": func(filename string) bool {
184			mimeType := mime.TypeByExtension(filepath.Ext(filename))
185			return strings.HasPrefix(mimeType, "image/")
186		},
187		"TabSizeClass": func(ec interface{}, filename string) string {
188			var (
189				value *editorconfig.Editorconfig
190				ok    bool
191			)
192			if ec != nil {
193				if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
194					return "tab-size-8"
195				}
196				def, err := value.GetDefinitionForFilename(filename)
197				if err != nil {
198					log.Error("tab size class: getting definition for filename: %v", err)
199					return "tab-size-8"
200				}
201				if def.TabWidth > 0 {
202					return fmt.Sprintf("tab-size-%d", def.TabWidth)
203				}
204			}
205			return "tab-size-8"
206		},
207		"SubJumpablePath": func(str string) []string {
208			var path []string
209			index := strings.LastIndex(str, "/")
210			if index != -1 && index != len(str) {
211				path = append(path, str[0:index+1], str[index+1:])
212			} else {
213				path = append(path, str)
214			}
215			return path
216		},
217		"DiffStatsWidth": func(adds int, dels int) string {
218			return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100)
219		},
220		"Json": func(in interface{}) string {
221			out, err := json.Marshal(in)
222			if err != nil {
223				return ""
224			}
225			return string(out)
226		},
227		"JsonPrettyPrint": func(in string) string {
228			var out bytes.Buffer
229			err := json.Indent(&out, []byte(in), "", "  ")
230			if err != nil {
231				return ""
232			}
233			return out.String()
234		},
235		"DisableGitHooks": func() bool {
236			return setting.DisableGitHooks
237		},
238		"DisableWebhooks": func() bool {
239			return setting.DisableWebhooks
240		},
241		"DisableImportLocal": func() bool {
242			return !setting.ImportLocalPaths
243		},
244		"Dict": func(values ...interface{}) (map[string]interface{}, error) {
245			if len(values)%2 != 0 {
246				return nil, errors.New("invalid dict call")
247			}
248			dict := make(map[string]interface{}, len(values)/2)
249			for i := 0; i < len(values); i += 2 {
250				key, ok := values[i].(string)
251				if !ok {
252					return nil, errors.New("dict keys must be strings")
253				}
254				dict[key] = values[i+1]
255			}
256			return dict, nil
257		},
258		"Printf":   fmt.Sprintf,
259		"Escape":   Escape,
260		"Sec2Time": models.SecToTime,
261		"ParseDeadline": func(deadline string) []string {
262			return strings.Split(deadline, "|")
263		},
264		"DefaultTheme": func() string {
265			return setting.UI.DefaultTheme
266		},
267		// pass key-value pairs to a partial template which receives them as a dict
268		"dict": func(values ...interface{}) (map[string]interface{}, error) {
269			if len(values) == 0 {
270				return nil, errors.New("invalid dict call")
271			}
272
273			dict := make(map[string]interface{})
274			return util.MergeInto(dict, values...)
275		},
276		/* like dict but merge key-value pairs into the first dict and return it */
277		"mergeinto": func(root map[string]interface{}, values ...interface{}) (map[string]interface{}, error) {
278			if len(values) == 0 {
279				return nil, errors.New("invalid mergeinto call")
280			}
281
282			dict := make(map[string]interface{})
283			for key, value := range root {
284				dict[key] = value
285			}
286
287			return util.MergeInto(dict, values...)
288		},
289		"percentage": func(n int, values ...int) float32 {
290			sum := 0
291			for i := 0; i < len(values); i++ {
292				sum += values[i]
293			}
294			return float32(n) * 100 / float32(sum)
295		},
296		"CommentMustAsDiff":   gitdiff.CommentMustAsDiff,
297		"MirrorRemoteAddress": mirrorRemoteAddress,
298		"NotificationSettings": func() map[string]interface{} {
299			return map[string]interface{}{
300				"MinTimeout":            int(setting.UI.Notification.MinTimeout / time.Millisecond),
301				"TimeoutStep":           int(setting.UI.Notification.TimeoutStep / time.Millisecond),
302				"MaxTimeout":            int(setting.UI.Notification.MaxTimeout / time.Millisecond),
303				"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
304			}
305		},
306		"containGeneric": func(arr interface{}, v interface{}) bool {
307			arrV := reflect.ValueOf(arr)
308			if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
309				return strings.Contains(arr.(string), v.(string))
310			}
311
312			if arrV.Kind() == reflect.Slice {
313				for i := 0; i < arrV.Len(); i++ {
314					iV := arrV.Index(i)
315					if !iV.CanInterface() {
316						continue
317					}
318					if iV.Interface() == v {
319						return true
320					}
321				}
322			}
323
324			return false
325		},
326		"contain": func(s []int64, id int64) bool {
327			for i := 0; i < len(s); i++ {
328				if s[i] == id {
329					return true
330				}
331			}
332			return false
333		},
334		"svg":            SVG,
335		"avatar":         Avatar,
336		"avatarHTML":     AvatarHTML,
337		"avatarByAction": AvatarByAction,
338		"avatarByEmail":  AvatarByEmail,
339		"repoAvatar":     RepoAvatar,
340		"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
341			// if needed
342			if len(normSort) == 0 || len(urlSort) == 0 {
343				return ""
344			}
345
346			if len(urlSort) == 0 && isDefault {
347				// if sort is sorted as default add arrow tho this table header
348				if isDefault {
349					return SVG("octicon-triangle-down", 16)
350				}
351			} else {
352				// if sort arg is in url test if it correlates with column header sort arguments
353				// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
354				if urlSort == normSort {
355					// the table is sorted with this header normal
356					return SVG("octicon-triangle-up", 16)
357				} else if urlSort == revSort {
358					// the table is sorted with this header reverse
359					return SVG("octicon-triangle-down", 16)
360				}
361			}
362			// the table is NOT sorted with this header
363			return ""
364		},
365		"RenderLabels": func(labels []*models.Label) template.HTML {
366			html := `<span class="labels-list">`
367			for _, label := range labels {
368				// Protect against nil value in labels - shouldn't happen but would cause a panic if so
369				if label == nil {
370					continue
371				}
372				html += fmt.Sprintf("<div class='ui label' style='color: %s; background-color: %s'>%s</div> ",
373					label.ForegroundColor(), label.Color, RenderEmoji(label.Name))
374			}
375			html += "</span>"
376			return template.HTML(html)
377		},
378		"MermaidMaxSourceCharacters": func() int {
379			return setting.MermaidMaxSourceCharacters
380		},
381		"Join":        strings.Join,
382		"QueryEscape": url.QueryEscape,
383		"DotEscape":   DotEscape,
384	}}
385}
386
387// NewTextFuncMap returns functions for injecting to text templates
388// It's a subset of those used for HTML and other templates
389func NewTextFuncMap() []texttmpl.FuncMap {
390	return []texttmpl.FuncMap{map[string]interface{}{
391		"GoVer": func() string {
392			return cases.Title(language.English).String(runtime.Version())
393		},
394		"AppName": func() string {
395			return setting.AppName
396		},
397		"AppSubUrl": func() string {
398			return setting.AppSubURL
399		},
400		"AppUrl": func() string {
401			return setting.AppURL
402		},
403		"AppVer": func() string {
404			return setting.AppVer
405		},
406		"AppBuiltWith": func() string {
407			return setting.AppBuiltWith
408		},
409		"AppDomain": func() string {
410			return setting.Domain
411		},
412		"TimeSince":     timeutil.TimeSince,
413		"TimeSinceUnix": timeutil.TimeSinceUnix,
414		"RawTimeSince":  timeutil.RawTimeSince,
415		"DateFmtLong": func(t time.Time) string {
416			return t.Format(time.RFC1123Z)
417		},
418		"DateFmtShort": func(t time.Time) string {
419			return t.Format("Jan 02, 2006")
420		},
421		"SubStr": func(str string, start, length int) string {
422			if len(str) == 0 {
423				return ""
424			}
425			end := start + length
426			if length == -1 {
427				end = len(str)
428			}
429			if len(str) < end {
430				return str
431			}
432			return str[start:end]
433		},
434		"EllipsisString": base.EllipsisString,
435		"URLJoin":        util.URLJoin,
436		"Dict": func(values ...interface{}) (map[string]interface{}, error) {
437			if len(values)%2 != 0 {
438				return nil, errors.New("invalid dict call")
439			}
440			dict := make(map[string]interface{}, len(values)/2)
441			for i := 0; i < len(values); i += 2 {
442				key, ok := values[i].(string)
443				if !ok {
444					return nil, errors.New("dict keys must be strings")
445				}
446				dict[key] = values[i+1]
447			}
448			return dict, nil
449		},
450		"Printf":   fmt.Sprintf,
451		"Escape":   Escape,
452		"Sec2Time": models.SecToTime,
453		"ParseDeadline": func(deadline string) []string {
454			return strings.Split(deadline, "|")
455		},
456		"dict": func(values ...interface{}) (map[string]interface{}, error) {
457			if len(values) == 0 {
458				return nil, errors.New("invalid dict call")
459			}
460
461			dict := make(map[string]interface{})
462
463			for i := 0; i < len(values); i++ {
464				switch key := values[i].(type) {
465				case string:
466					i++
467					if i == len(values) {
468						return nil, errors.New("specify the key for non array values")
469					}
470					dict[key] = values[i]
471				case map[string]interface{}:
472					m := values[i].(map[string]interface{})
473					for i, v := range m {
474						dict[i] = v
475					}
476				default:
477					return nil, errors.New("dict values must be maps")
478				}
479			}
480			return dict, nil
481		},
482		"percentage": func(n int, values ...int) float32 {
483			sum := 0
484			for i := 0; i < len(values); i++ {
485				sum += values[i]
486			}
487			return float32(n) * 100 / float32(sum)
488		},
489		"Add": func(a ...int) int {
490			sum := 0
491			for _, val := range a {
492				sum += val
493			}
494			return sum
495		},
496		"Mul": func(a ...int) int {
497			sum := 1
498			for _, val := range a {
499				sum *= val
500			}
501			return sum
502		},
503		"QueryEscape": url.QueryEscape,
504	}}
505}
506
507var (
508	widthRe  = regexp.MustCompile(`width="[0-9]+?"`)
509	heightRe = regexp.MustCompile(`height="[0-9]+?"`)
510)
511
512func parseOthers(defaultSize int, defaultClass string, others ...interface{}) (int, string) {
513	size := defaultSize
514	if len(others) > 0 && others[0].(int) != 0 {
515		size = others[0].(int)
516	}
517
518	class := defaultClass
519	if len(others) > 1 && others[1].(string) != "" {
520		if defaultClass == "" {
521			class = others[1].(string)
522		} else {
523			class = defaultClass + " " + others[1].(string)
524		}
525	}
526
527	return size, class
528}
529
530// AvatarHTML creates the HTML for an avatar
531func AvatarHTML(src string, size int, class, name string) template.HTML {
532	sizeStr := fmt.Sprintf(`%d`, size)
533
534	if name == "" {
535		name = "avatar"
536	}
537
538	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
539}
540
541// SVG render icons - arguments icon name (string), size (int), class (string)
542func SVG(icon string, others ...interface{}) template.HTML {
543	size, class := parseOthers(16, "", others...)
544
545	if svgStr, ok := svg.SVGs[icon]; ok {
546		if size != 16 {
547			svgStr = widthRe.ReplaceAllString(svgStr, fmt.Sprintf(`width="%d"`, size))
548			svgStr = heightRe.ReplaceAllString(svgStr, fmt.Sprintf(`height="%d"`, size))
549		}
550		if class != "" {
551			svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1)
552		}
553		return template.HTML(svgStr)
554	}
555	return template.HTML("")
556}
557
558// Avatar renders user avatars. args: user, size (int), class (string)
559func Avatar(item interface{}, others ...interface{}) template.HTML {
560	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...)
561
562	switch t := item.(type) {
563	case *user_model.User:
564		src := t.AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
565		if src != "" {
566			return AvatarHTML(src, size, class, t.DisplayName())
567		}
568	case *models.Collaborator:
569		src := t.AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
570		if src != "" {
571			return AvatarHTML(src, size, class, t.DisplayName())
572		}
573	case *models.Organization:
574		src := t.AsUser().AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor)
575		if src != "" {
576			return AvatarHTML(src, size, class, t.AsUser().DisplayName())
577		}
578	}
579
580	return template.HTML("")
581}
582
583// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
584func AvatarByAction(action *models.Action, others ...interface{}) template.HTML {
585	action.LoadActUser()
586	return Avatar(action.ActUser, others...)
587}
588
589// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
590func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
591	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...)
592
593	src := repo.RelAvatarLink()
594	if src != "" {
595		return AvatarHTML(src, size, class, repo.FullName())
596	}
597	return template.HTML("")
598}
599
600// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
601func AvatarByEmail(email, name string, others ...interface{}) template.HTML {
602	size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar image", others...)
603	src := avatars.GenerateEmailAvatarFastLink(email, size*setting.Avatar.RenderedSizeFactor)
604
605	if src != "" {
606		return AvatarHTML(src, size, class, name)
607	}
608
609	return template.HTML("")
610}
611
612// Safe render raw as HTML
613func Safe(raw string) template.HTML {
614	return template.HTML(raw)
615}
616
617// SafeJS renders raw as JS
618func SafeJS(raw string) template.JS {
619	return template.JS(raw)
620}
621
622// Str2html render Markdown text to HTML
623func Str2html(raw string) template.HTML {
624	return template.HTML(markup.Sanitize(raw))
625}
626
627// Escape escapes a HTML string
628func Escape(raw string) string {
629	return html.EscapeString(raw)
630}
631
632// JSEscape escapes a JS string
633func JSEscape(raw string) string {
634	return template.JSEscapeString(raw)
635}
636
637// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
638func DotEscape(raw string) string {
639	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
640}
641
642// Sha1 returns sha1 sum of string
643func Sha1(str string) string {
644	return base.EncodeSha1(str)
645}
646
647// RenderCommitMessage renders commit message with XSS-safe and special links.
648func RenderCommitMessage(msg, urlPrefix string, metas map[string]string) template.HTML {
649	return RenderCommitMessageLink(msg, urlPrefix, "", metas)
650}
651
652// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
653// default url, handling for special links.
654func RenderCommitMessageLink(msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
655	cleanMsg := template.HTMLEscapeString(msg)
656	// we can safely assume that it will not return any error, since there
657	// shouldn't be any special HTML.
658	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
659		URLPrefix:   urlPrefix,
660		DefaultLink: urlDefault,
661		Metas:       metas,
662	}, cleanMsg)
663	if err != nil {
664		log.Error("RenderCommitMessage: %v", err)
665		return ""
666	}
667	msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n")
668	if len(msgLines) == 0 {
669		return template.HTML("")
670	}
671	return template.HTML(msgLines[0])
672}
673
674// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
675// the provided default url, handling for special links without email to links.
676func RenderCommitMessageLinkSubject(msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
677	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
678	lineEnd := strings.IndexByte(msgLine, '\n')
679	if lineEnd > 0 {
680		msgLine = msgLine[:lineEnd]
681	}
682	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
683	if len(msgLine) == 0 {
684		return template.HTML("")
685	}
686
687	// we can safely assume that it will not return any error, since there
688	// shouldn't be any special HTML.
689	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
690		URLPrefix:   urlPrefix,
691		DefaultLink: urlDefault,
692		Metas:       metas,
693	}, template.HTMLEscapeString(msgLine))
694	if err != nil {
695		log.Error("RenderCommitMessageSubject: %v", err)
696		return template.HTML("")
697	}
698	return template.HTML(renderedMessage)
699}
700
701// RenderCommitBody extracts the body of a commit message without its title.
702func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.HTML {
703	msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
704	lineEnd := strings.IndexByte(msgLine, '\n')
705	if lineEnd > 0 {
706		msgLine = msgLine[lineEnd+1:]
707	} else {
708		return template.HTML("")
709	}
710	msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
711	if len(msgLine) == 0 {
712		return template.HTML("")
713	}
714
715	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
716		URLPrefix: urlPrefix,
717		Metas:     metas,
718	}, template.HTMLEscapeString(msgLine))
719	if err != nil {
720		log.Error("RenderCommitMessage: %v", err)
721		return ""
722	}
723	return template.HTML(renderedMessage)
724}
725
726// RenderIssueTitle renders issue/pull title with defined post processors
727func RenderIssueTitle(text, urlPrefix string, metas map[string]string) template.HTML {
728	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
729		URLPrefix: urlPrefix,
730		Metas:     metas,
731	}, template.HTMLEscapeString(text))
732	if err != nil {
733		log.Error("RenderIssueTitle: %v", err)
734		return template.HTML("")
735	}
736	return template.HTML(renderedText)
737}
738
739// RenderEmoji renders html text with emoji post processors
740func RenderEmoji(text string) template.HTML {
741	renderedText, err := markup.RenderEmoji(template.HTMLEscapeString(text))
742	if err != nil {
743		log.Error("RenderEmoji: %v", err)
744		return template.HTML("")
745	}
746	return template.HTML(renderedText)
747}
748
749// ReactionToEmoji renders emoji for use in reactions
750func ReactionToEmoji(reaction string) template.HTML {
751	val := emoji.FromCode(reaction)
752	if val != nil {
753		return template.HTML(val.Emoji)
754	}
755	val = emoji.FromAlias(reaction)
756	if val != nil {
757		return template.HTML(val.Emoji)
758	}
759	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
760}
761
762// RenderNote renders the contents of a git-notes file as a commit message.
763func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML {
764	cleanMsg := template.HTMLEscapeString(msg)
765	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
766		URLPrefix: urlPrefix,
767		Metas:     metas,
768	}, cleanMsg)
769	if err != nil {
770		log.Error("RenderNote: %v", err)
771		return ""
772	}
773	return template.HTML(string(fullMessage))
774}
775
776// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
777func IsMultilineCommitMessage(msg string) bool {
778	return strings.Count(strings.TrimSpace(msg), "\n") >= 1
779}
780
781// Actioner describes an action
782type Actioner interface {
783	GetOpType() models.ActionType
784	GetActUserName() string
785	GetRepoUserName() string
786	GetRepoName() string
787	GetRepoPath() string
788	GetRepoLink() string
789	GetBranch() string
790	GetContent() string
791	GetCreate() time.Time
792	GetIssueInfos() []string
793}
794
795// ActionIcon accepts an action operation type and returns an icon class name.
796func ActionIcon(opType models.ActionType) string {
797	switch opType {
798	case models.ActionCreateRepo, models.ActionTransferRepo, models.ActionRenameRepo:
799		return "repo"
800	case models.ActionCommitRepo, models.ActionPushTag, models.ActionDeleteTag, models.ActionDeleteBranch:
801		return "git-commit"
802	case models.ActionCreateIssue:
803		return "issue-opened"
804	case models.ActionCreatePullRequest:
805		return "git-pull-request"
806	case models.ActionCommentIssue, models.ActionCommentPull:
807		return "comment-discussion"
808	case models.ActionMergePullRequest:
809		return "git-merge"
810	case models.ActionCloseIssue, models.ActionClosePullRequest:
811		return "issue-closed"
812	case models.ActionReopenIssue, models.ActionReopenPullRequest:
813		return "issue-reopened"
814	case models.ActionMirrorSyncPush, models.ActionMirrorSyncCreate, models.ActionMirrorSyncDelete:
815		return "mirror"
816	case models.ActionApprovePullRequest:
817		return "check"
818	case models.ActionRejectPullRequest:
819		return "diff"
820	case models.ActionPublishRelease:
821		return "tag"
822	case models.ActionPullReviewDismissed:
823		return "x"
824	default:
825		return "question"
826	}
827}
828
829// ActionContent2Commits converts action content to push commits
830func ActionContent2Commits(act Actioner) *repository.PushCommits {
831	push := repository.NewPushCommits()
832
833	if act == nil || act.GetContent() == "" {
834		return push
835	}
836
837	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
838		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
839	}
840
841	if push.Len == 0 {
842		push.Len = len(push.Commits)
843	}
844
845	return push
846}
847
848// DiffTypeToStr returns diff type name
849func DiffTypeToStr(diffType int) string {
850	diffTypes := map[int]string{
851		1: "add", 2: "modify", 3: "del", 4: "rename", 5: "copy",
852	}
853	return diffTypes[diffType]
854}
855
856// DiffLineTypeToStr returns diff line type name
857func DiffLineTypeToStr(diffType int) string {
858	switch diffType {
859	case 2:
860		return "add"
861	case 3:
862		return "del"
863	case 4:
864		return "tag"
865	}
866	return "same"
867}
868
869// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
870func MigrationIcon(hostname string) string {
871	switch hostname {
872	case "github.com":
873		return "octicon-mark-github"
874	default:
875		return "gitea-git"
876	}
877}
878
879func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
880	// Split template into subject and body
881	var subjectContent []byte
882	bodyContent := content
883	loc := mailSubjectSplit.FindIndex(content)
884	if loc != nil {
885		subjectContent = content[0:loc[0]]
886		bodyContent = content[loc[1]:]
887	}
888	if _, err := stpl.New(name).
889		Parse(string(subjectContent)); err != nil {
890		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
891	}
892	if _, err := btpl.New(name).
893		Parse(string(bodyContent)); err != nil {
894		log.Warn("Failed to parse template [%s/body]: %v", name, err)
895	}
896}
897
898type remoteAddress struct {
899	Address  string
900	Username string
901	Password string
902}
903
904func mirrorRemoteAddress(m repo_model.RemoteMirrorer) remoteAddress {
905	a := remoteAddress{}
906
907	u, err := git.GetRemoteAddress(git.DefaultContext, m.GetRepository().RepoPath(), m.GetRemoteName())
908	if err != nil {
909		log.Error("GetRemoteAddress %v", err)
910		return a
911	}
912
913	if u.User != nil {
914		a.Username = u.User.Username()
915		a.Password, _ = u.User.Password()
916	}
917	u.User = nil
918	a.Address = u.String()
919
920	return a
921}
922