From 042e9fcd810bfd083adaddc7bdf5d5edc2fed0d3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 19 Jun 2024 11:23:24 +0800
Subject: [PATCH] Fix rendered wiki page link (#31398) (#31407)

Backport #31398

Fix #31395
---
 modules/markup/html.go                    | 27 ++---------------
 modules/markup/html_link.go               | 35 +++++++++++++++++++++++
 modules/markup/html_test.go               |  4 +++
 modules/markup/markdown/goldmark.go       |  2 +-
 modules/markup/markdown/markdown_test.go  | 10 +++----
 modules/markup/markdown/transform_link.go | 32 ++-------------------
 modules/markup/renderer.go                | 12 ++------
 modules/templates/util_render_test.go     |  4 +--
 routers/web/repo/render.go                |  2 +-
 routers/web/repo/view.go                  | 10 +++----
 routers/web/repo/wiki.go                  |  2 +-
 11 files changed, 62 insertions(+), 78 deletions(-)
 create mode 100644 modules/markup/html_link.go

diff --git a/modules/markup/html.go b/modules/markup/html.go
index b436ff6c3d..1eedf095a0 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -761,10 +761,10 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			if image {
 				link = strings.ReplaceAll(link, " ", "+")
 			} else {
-				link = strings.ReplaceAll(link, " ", "-")
+				link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
 			}
 			if !strings.Contains(link, "/") {
-				link = url.PathEscape(link)
+				link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping
 			}
 		}
 		if image {
@@ -796,28 +796,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 				childNode.Attr = childNode.Attr[:2]
 			}
 		} else {
-			if !absoluteLink {
-				var base string
-				if ctx.IsWiki {
-					switch ext {
-					case "":
-						// no file extension, create a regular wiki link
-						base = ctx.Links.WikiLink()
-					default:
-						// we have a file extension:
-						// return a regular wiki link if it's a renderable file (extension),
-						// raw link otherwise
-						if Type(link) != "" {
-							base = ctx.Links.WikiLink()
-						} else {
-							base = ctx.Links.WikiRawLink()
-						}
-					}
-				} else {
-					base = ctx.Links.SrcLink()
-				}
-				link = util.URLJoin(base, link)
-			}
+			link, _ = ResolveLink(ctx, link, "")
 			childNode.Type = html.TextNode
 			childNode.Data = name
 		}
diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go
new file mode 100644
index 0000000000..a41b87e9fa
--- /dev/null
+++ b/modules/markup/html_link.go
@@ -0,0 +1,35 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"path"
+
+	"code.gitea.io/gitea/modules/util"
+)
+
+func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
+	isAnchorFragment := link != "" && link[0] == '#'
+	if !isAnchorFragment && !IsFullURLString(link) {
+		linkBase := ctx.Links.Base
+		if ctx.IsWiki {
+			if ext := path.Ext(link); ext == "" || ext == ".-" {
+				linkBase = ctx.Links.WikiLink() // the link is for a wiki page
+			} else if DetectMarkupTypeByFileName(link) != "" {
+				linkBase = ctx.Links.WikiLink() // the link is renderable as a wiki page
+			} else {
+				linkBase = ctx.Links.WikiRawLink() // otherwise, use a raw link instead to view&download medias
+			}
+		} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
+			// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
+			// and then this link will be handled by the "legacy-ref" code and be redirected to the default branch like "/owner/repo/src/branch/main/{the-file-path}"
+			linkBase = ctx.Links.SrcLink()
+		}
+		link, resolved = util.URLJoin(linkBase, link), true
+	}
+	if isAnchorFragment && userContentAnchorPrefix != "" {
+		link, resolved = userContentAnchorPrefix+link[1:], true
+	}
+	return link, resolved
+}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index a0642bcfa4..64cc30d246 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -442,6 +442,10 @@ func TestRender_ShortLinks(t *testing.T) {
 		"[[Link]]",
 		`<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
 		`<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`)
+	test(
+		"[[Link.-]]",
+		`<p><a href="http://localhost:3000/test-owner/test-repo/src/master/Link.-" rel="nofollow">Link.-</a></p>`,
+		`<p><a href="http://localhost:3000/test-owner/test-repo/wiki/Link.-" rel="nofollow">Link.-</a></p>`)
 	test(
 		"[[Link.jpg]]",
 		`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Link.jpg" alt="Link.jpg"/></a></p>`,
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index b8b3aeaab0..ab11a56c4d 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -67,7 +67,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 		case *ast.Image:
 			g.transformImage(ctx, v, reader)
 		case *ast.Link:
-			g.transformLink(ctx, v, reader)
+			g.transformLink(ctx, v)
 		case *ast.List:
 			g.transformList(ctx, v, reader, rc)
 		case *ast.Text:
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index cefbdbf9c2..ecd747214b 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -626,7 +626,7 @@ mail@domain.com
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
 <a href="/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
 <a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
@@ -682,7 +682,7 @@ space</p>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
 <a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
 <a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
@@ -740,7 +740,7 @@ space</p>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
 <a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
@@ -857,7 +857,7 @@ space</p>
 			Expected: `<p>space @mention-user<br/>
 /just/a/path.bin<br/>
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
-<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
+<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
@@ -975,7 +975,7 @@ space</p>
 	for i, c := range cases {
 		result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
 		assert.NoError(t, err, "Unexpected error in testcase: %v", i)
-		assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
+		assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
 	}
 }
 
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
index 7e305b74bc..38fbf693ab 100644
--- a/modules/markup/markdown/transform_link.go
+++ b/modules/markup/markdown/transform_link.go
@@ -4,39 +4,13 @@
 package markdown
 
 import (
-	"path/filepath"
-
 	"code.gitea.io/gitea/modules/markup"
-	giteautil "code.gitea.io/gitea/modules/util"
 
 	"github.com/yuin/goldmark/ast"
-	"github.com/yuin/goldmark/text"
 )
 
-func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
-	// Links need their href to munged to be a real value
-	link := v.Destination
-	isAnchorFragment := len(link) > 0 && link[0] == '#'
-	if !isAnchorFragment && !markup.IsFullURLBytes(link) {
-		base := ctx.Links.Base
-		if ctx.IsWiki {
-			if filepath.Ext(string(link)) == "" {
-				// This link doesn't have a file extension - assume a regular wiki link
-				base = ctx.Links.WikiLink()
-			} else if markup.Type(string(link)) != "" {
-				// If it's a file type we can render, use a regular wiki link
-				base = ctx.Links.WikiLink()
-			} else {
-				// Otherwise, use a raw link instead
-				base = ctx.Links.WikiRawLink()
-			}
-		} else if ctx.Links.HasBranchInfo() {
-			base = ctx.Links.SrcLink()
-		}
-		link = []byte(giteautil.URLJoin(base, string(link)))
+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
+	if link, resolved := markup.ResolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
+		v.Destination = []byte(link)
 	}
-	if isAnchorFragment {
-		link = []byte("#user-content-" + string(link)[1:])
-	}
-	v.Destination = link
 }
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 3284a8194e..c18ba918da 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -370,22 +370,14 @@ func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
 	return ErrUnsupportedRenderExtension{extension}
 }
 
-// Type returns if markup format via the filename
-func Type(filename string) string {
+// DetectMarkupTypeByFileName returns the possible markup format type via the filename
+func DetectMarkupTypeByFileName(filename string) string {
 	if parser := GetRendererByFileName(filename); parser != nil {
 		return parser.Name()
 	}
 	return ""
 }
 
-// IsMarkupFile reports whether file is a markup type file
-func IsMarkupFile(name, markup string) bool {
-	if parser := GetRendererByFileName(name); parser != nil {
-		return parser.Name() == markup
-	}
-	return false
-}
-
 func PreviewableExtensions() []string {
 	extensions := make([]string, 0, len(extRenderers))
 	for extension := range extRenderers {
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index f493b899e3..32e53b5215 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -174,7 +174,7 @@ func TestRenderMarkdownToHtml(t *testing.T) {
 <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
 <a href="/file.bin" rel="nofollow">local link</a>
 <a href="https://example.com" rel="nofollow">remote link</a>
-<a href="/src/file.bin" rel="nofollow">local link</a>
+<a href="/file.bin" rel="nofollow">local link</a>
 <a href="https://example.com" rel="nofollow">remote link</a>
 <a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
 <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
@@ -190,7 +190,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 #123
 space</p>
 `
-	assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput()))
+	assert.Equal(t, expected, string(RenderMarkdownToHtml(context.Background(), testInput())))
 }
 
 func TestRenderLabels(t *testing.T) {
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index e64db03e20..6aba9e0ac1 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -47,7 +47,7 @@ func RenderFile(ctx *context.Context) {
 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 
-	if markupType := markup.Type(blob.Name()); markupType == "" {
+	if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType == "" {
 		if isTextFile {
 			_, _ = io.Copy(ctx.Resp, rd)
 		} else {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 386ef7be5c..da849d7b72 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -307,7 +307,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 
 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
-	if markupType := markup.Type(readmeFile.Name()); markupType != "" {
+	if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
 		ctx.Data["IsMarkup"] = true
 		ctx.Data["MarkupType"] = markupType
 
@@ -499,7 +499,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 		readmeExist := util.IsReadmeFileName(blob.Name())
 		ctx.Data["ReadmeExist"] = readmeExist
 
-		markupType := markup.Type(blob.Name())
+		markupType := markup.DetectMarkupTypeByFileName(blob.Name())
 		// If the markup is detected by custom markup renderer it should not be reset later on
 		// to not pass it down to the render context.
 		detected := false
@@ -606,9 +606,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			break
 		}
 
-		// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
-		// maybe for this case, the file is a binary file, and shouldn't be rendered?
-		if markupType := markup.Type(blob.Name()); markupType != "" {
+		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+		// It is used by "external renders", markupRender will execute external programs to get rendered content.
+		if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
 			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 			ctx.Data["IsMarkup"] = true
 			ctx.Data["MarkupType"] = markupType
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index df15f61b17..ff6397cd2a 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -532,7 +532,7 @@ func Wiki(ctx *context.Context) {
 	}
 
 	wikiPath := entry.Name()
-	if markup.Type(wikiPath) != markdown.MarkupName {
+	if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
 		ext := strings.ToUpper(filepath.Ext(wikiPath))
 		ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
 	}