mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:54:53 -05:00
875b398e5e
Backport #26745
Fixes #26548
This PR refactors the rendering of markup links. The old code uses
`strings.Replace` to change some urls while the new code uses more
context to decide which link should be generated.
The added tests should ensure the same output for the old and new
behaviour (besides the bug).
We may need to refactor the rendering a bit more to make it clear how
the different helper methods render the input string. There are lots of
options (resolve links / images / mentions / git hashes / emojis / ...)
but you don't really know what helper uses which options. For example,
we currently support images in the user description which should not be
allowed I think:
<details>
<summary>Profile</summary>
https://try.gitea.io/KN4CK3R
![grafik](https://github.com/go-gitea/gitea/assets/1666336/109ae422-496d-4200-b52e-b3a528f553e5)
</details>
(cherry picked from commit 022552d5b6
)
226 lines
7.6 KiB
Go
226 lines
7.6 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package templates
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
"code.gitea.io/gitea/modules/emoji"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/markup"
|
|
"code.gitea.io/gitea/modules/markup/markdown"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
|
func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
|
// we can safely assume that it will not return any error, since there
|
|
// shouldn't be any special HTML.
|
|
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
Metas: metas,
|
|
}, cleanMsg)
|
|
if err != nil {
|
|
log.Error("RenderCommitMessage: %v", err)
|
|
return ""
|
|
}
|
|
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
|
if len(msgLines) == 0 {
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(msgLines[0])
|
|
}
|
|
|
|
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
|
|
// the provided default url, handling for special links without email to links.
|
|
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
|
|
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
|
|
lineEnd := strings.IndexByte(msgLine, '\n')
|
|
if lineEnd > 0 {
|
|
msgLine = msgLine[:lineEnd]
|
|
}
|
|
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
|
if len(msgLine) == 0 {
|
|
return template.HTML("")
|
|
}
|
|
|
|
// we can safely assume that it will not return any error, since there
|
|
// shouldn't be any special HTML.
|
|
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
DefaultLink: urlDefault,
|
|
Metas: metas,
|
|
}, template.HTMLEscapeString(msgLine))
|
|
if err != nil {
|
|
log.Error("RenderCommitMessageSubject: %v", err)
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(renderedMessage)
|
|
}
|
|
|
|
// RenderCommitBody extracts the body of a commit message without its title.
|
|
func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML {
|
|
msgLine := strings.TrimSpace(msg)
|
|
lineEnd := strings.IndexByte(msgLine, '\n')
|
|
if lineEnd > 0 {
|
|
msgLine = msgLine[lineEnd+1:]
|
|
} else {
|
|
return ""
|
|
}
|
|
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
|
|
if len(msgLine) == 0 {
|
|
return ""
|
|
}
|
|
|
|
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
Metas: metas,
|
|
}, template.HTMLEscapeString(msgLine))
|
|
if err != nil {
|
|
log.Error("RenderCommitMessage: %v", err)
|
|
return ""
|
|
}
|
|
return template.HTML(renderedMessage)
|
|
}
|
|
|
|
// Match text that is between back ticks.
|
|
var codeMatcher = regexp.MustCompile("`([^`]+)`")
|
|
|
|
// RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles
|
|
func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
|
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags
|
|
return template.HTML(htmlWithCodeTags)
|
|
}
|
|
|
|
// RenderIssueTitle renders issue/pull title with defined post processors
|
|
func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML {
|
|
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
Metas: metas,
|
|
}, template.HTMLEscapeString(text))
|
|
if err != nil {
|
|
log.Error("RenderIssueTitle: %v", err)
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(renderedText)
|
|
}
|
|
|
|
// RenderLabel renders a label
|
|
func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
|
|
labelScope := label.ExclusiveScope()
|
|
|
|
textColor := "#111"
|
|
r, g, b := util.HexToRBGColor(label.Color)
|
|
// Determine if label text should be light or dark to be readable on background color
|
|
if util.UseLightTextOnBackground(r, g, b) {
|
|
textColor = "#eee"
|
|
}
|
|
|
|
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
|
|
|
if labelScope == "" {
|
|
// Regular label
|
|
s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' data-tooltip-content title='%s'>%s</div>",
|
|
textColor, label.Color, description, RenderEmoji(ctx, label.Name))
|
|
return template.HTML(s)
|
|
}
|
|
|
|
// Scoped label
|
|
scopeText := RenderEmoji(ctx, labelScope)
|
|
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
|
|
|
// Make scope and item background colors slightly darker and lighter respectively.
|
|
// More contrast needed with higher luminance, empirically tweaked.
|
|
luminance := util.GetLuminance(r, g, b)
|
|
contrast := 0.01 + luminance*0.03
|
|
// Ensure we add the same amount of contrast also near 0 and 1.
|
|
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
|
lighten := contrast + math.Max(contrast-luminance, 0.0)
|
|
// Compute factor to keep RGB values proportional.
|
|
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
|
|
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
|
|
|
|
scopeBytes := []byte{
|
|
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
|
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
|
uint8(math.Min(math.Round(b*darkenFactor), 255)),
|
|
}
|
|
itemBytes := []byte{
|
|
uint8(math.Min(math.Round(r*lightenFactor), 255)),
|
|
uint8(math.Min(math.Round(g*lightenFactor), 255)),
|
|
uint8(math.Min(math.Round(b*lightenFactor), 255)),
|
|
}
|
|
|
|
itemColor := "#" + hex.EncodeToString(itemBytes)
|
|
scopeColor := "#" + hex.EncodeToString(scopeBytes)
|
|
|
|
s := fmt.Sprintf("<span class='ui label scope-parent' data-tooltip-content title='%s'>"+
|
|
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
|
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
|
"</span>",
|
|
description,
|
|
textColor, scopeColor, scopeText,
|
|
textColor, itemColor, itemText)
|
|
return template.HTML(s)
|
|
}
|
|
|
|
// RenderEmoji renders html text with emoji post processors
|
|
func RenderEmoji(ctx context.Context, text string) template.HTML {
|
|
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
|
|
template.HTMLEscapeString(text))
|
|
if err != nil {
|
|
log.Error("RenderEmoji: %v", err)
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(renderedText)
|
|
}
|
|
|
|
// ReactionToEmoji renders emoji for use in reactions
|
|
func ReactionToEmoji(reaction string) template.HTML {
|
|
val := emoji.FromCode(reaction)
|
|
if val != nil {
|
|
return template.HTML(val.Emoji)
|
|
}
|
|
val = emoji.FromAlias(reaction)
|
|
if val != nil {
|
|
return template.HTML(val.Emoji)
|
|
}
|
|
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
|
|
}
|
|
|
|
func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
|
|
output, err := markdown.RenderString(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
Metas: map[string]string{"mode": "document"},
|
|
}, input)
|
|
if err != nil {
|
|
log.Error("RenderString: %v", err)
|
|
}
|
|
return template.HTML(output)
|
|
}
|
|
|
|
func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
|
htmlCode := `<span class="labels-list">`
|
|
for _, label := range labels {
|
|
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
|
if label == nil {
|
|
continue
|
|
}
|
|
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
|
repoLink, label.ID, RenderLabel(ctx, label))
|
|
}
|
|
htmlCode += "</span>"
|
|
return template.HTML(htmlCode)
|
|
}
|