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