From 3fe1f732686989095abd25599c1dc75bda46246d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 17 Aug 2024 11:19:26 +0800
Subject: [PATCH] Fix raw wiki links (#31825) (#31845)

Backport #31825 by @Zettat123

Fix #31395

This regression is introduced by #30273. To find out how GitHub handles
this case, I did [some
tests](https://github.com/go-gitea/gitea/issues/31395#issuecomment-2278929115).

I use redirect in this PR instead of checking if the corresponding `.md`
file exists when rendering the link because GitHub also uses redirect.
With this PR, there is no need to resolve the raw wiki link when
rendering a wiki page. If a wiki link points to a raw file, access will
be redirected to the raw link.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 modules/markup/html_link.go              | 12 ++---
 modules/markup/html_test.go              |  2 +-
 modules/markup/markdown/markdown_test.go | 24 ++++-----
 routers/web/repo/wiki.go                 | 64 ++++++++++++++++++++----
 routers/web/repo/wiki_test.go            | 14 ++++++
 5 files changed, 84 insertions(+), 32 deletions(-)

diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go
index a41b87e9fa8..b0861353485 100644
--- a/modules/markup/html_link.go
+++ b/modules/markup/html_link.go
@@ -4,8 +4,6 @@
 package markup
 
 import (
-	"path"
-
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -14,13 +12,9 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu
 	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
-			}
+			// no need to check if the link should be resolved as a wiki link or a wiki raw link
+			// just use wiki link here and it will be redirected to a wiki raw link if necessary
+			linkBase = ctx.Links.WikiLink()
 		} 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}"
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 8911bf3f2ef..85ed78a922a 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -435,7 +435,7 @@ func TestRender_ShortLinks(t *testing.T) {
 	renderableFileURL := util.URLJoin(tree, "markdown_file.md")
 	renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md")
 	unrenderableFileURL := util.URLJoin(tree, "file.zip")
-	unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "file.zip")
+	unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "file.zip")
 	favicon := "http://google.com/favicon.ico"
 
 	test(
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index d71ca363864..14344d91550 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -655,9 +655,9 @@ 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="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -713,9 +713,9 @@ 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="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -771,9 +771,9 @@ 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="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -831,9 +831,9 @@ 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="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -891,9 +891,9 @@ 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="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
@@ -953,9 +953,9 @@ 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="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
-<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
 <a href="https://example.com" rel="nofollow">remote link</a><br/>
 <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
 <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 13b6a7b8e3b..d2056353d88 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -138,18 +138,41 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
 	return content
 }
 
-// wikiContentsByName returns the contents of a wiki page, along with a boolean
-// indicating whether the page exists. Writes to ctx if an error occurs.
-func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) {
+// wikiEntryByName returns the entry of a wiki page, along with a boolean
+// indicating whether the entry exists. Writes to ctx if an error occurs.
+// The last return value indicates whether the file should be returned as a raw file
+func wikiEntryByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) (*git.TreeEntry, string, bool, bool) {
+	isRaw := false
 	gitFilename := wiki_service.WebPathToGitPath(wikiName)
 	entry, err := findEntryForFile(commit, gitFilename)
 	if err != nil && !git.IsErrNotExist(err) {
 		ctx.ServerError("findEntryForFile", err)
-		return nil, nil, "", false
-	} else if entry == nil {
+		return nil, "", false, false
+	}
+	if entry == nil {
+		// check if the file without ".md" suffix exists
+		gitFilename := strings.TrimSuffix(gitFilename, ".md")
+		entry, err = findEntryForFile(commit, gitFilename)
+		if err != nil && !git.IsErrNotExist(err) {
+			ctx.ServerError("findEntryForFile", err)
+			return nil, "", false, false
+		}
+		isRaw = true
+	}
+	if entry == nil {
+		return nil, "", true, false
+	}
+	return entry, gitFilename, false, isRaw
+}
+
+// wikiContentsByName returns the contents of a wiki page, along with a boolean
+// indicating whether the page exists. Writes to ctx if an error occurs.
+func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) {
+	entry, gitFilename, noEntry, _ := wikiEntryByName(ctx, commit, wikiName)
+	if entry == nil {
 		return nil, nil, "", true
 	}
-	return wikiContentsByEntry(ctx, entry), entry, gitFilename, false
+	return wikiContentsByEntry(ctx, entry), entry, gitFilename, noEntry
 }
 
 func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
@@ -215,11 +238,14 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 	isSideBar := pageName == "_Sidebar"
 	isFooter := pageName == "_Footer"
 
-	// lookup filename in wiki - get filecontent, gitTree entry , real filename
-	data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+	// lookup filename in wiki - get gitTree entry , real filename
+	entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
 	if noEntry {
 		ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
 	}
+	if isRaw {
+		ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
+	}
 	if entry == nil || ctx.Written() {
 		if wikiRepo != nil {
 			wikiRepo.Close()
@@ -227,6 +253,15 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 		return nil, nil
 	}
 
+	// get filecontent
+	data := wikiContentsByEntry(ctx, entry)
+	if ctx.Written() {
+		if wikiRepo != nil {
+			wikiRepo.Close()
+		}
+		return nil, nil
+	}
+
 	var sidebarContent []byte
 	if !isSideBar {
 		sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar")
@@ -442,15 +477,24 @@ func renderEditPage(ctx *context.Context) {
 	ctx.Data["Title"] = displayName
 	ctx.Data["title"] = displayName
 
-	// lookup filename in wiki - get filecontent, gitTree entry , real filename
-	data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
+	// lookup filename in wiki -  gitTree entry , real filename
+	entry, _, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
 	if noEntry {
 		ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
 	}
+	if isRaw {
+		ctx.Error(http.StatusForbidden, "Editing of raw wiki files is not allowed")
+	}
 	if entry == nil || ctx.Written() {
 		return
 	}
 
+	// get filecontent
+	data := wikiContentsByEntry(ctx, entry)
+	if ctx.Written() {
+		return
+	}
+
 	ctx.Data["content"] = string(data)
 	ctx.Data["sidebarPresent"] = false
 	ctx.Data["sidebarContent"] = ""
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 4602dcfeb40..86971f15f11 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -87,6 +87,13 @@ func TestWiki(t *testing.T) {
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
 	assert.EqualValues(t, "Home", ctx.Data["Title"])
 	assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
+
+	ctx, _ = contexttest.MockContext(t, "user2/repo1/jpeg.jpg")
+	ctx.SetParams("*", "jpeg.jpg")
+	contexttest.LoadRepo(t, ctx, 1)
+	Wiki(ctx)
+	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+	assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location"))
 }
 
 func TestWikiPages(t *testing.T) {
@@ -160,6 +167,13 @@ func TestEditWiki(t *testing.T) {
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
 	assert.EqualValues(t, "Home", ctx.Data["Title"])
 	assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
+
+	ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit")
+	ctx.SetParams("*", "jpeg.jpg")
+	contexttest.LoadUser(t, ctx, 2)
+	contexttest.LoadRepo(t, ctx, 1)
+	EditWiki(ctx)
+	assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status())
 }
 
 func TestEditWikiPost(t *testing.T) {