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 }