From 597d1da96b92b181c106813ce26149334b2b44e5 Mon Sep 17 00:00:00 2001
From: Brecht Van Lommel <brecht@blender.org>
Date: Mon, 17 Jun 2024 08:16:14 +0200
Subject: [PATCH] Fix missing images in editor preview due to wrong links
 (#31299)

Parse base path and tree path so that media links can be correctly
created with /media/.

Resolves #31294

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/renderer.go         |   8 +--
 modules/structs/miscellaneous.go   |   6 +-
 routers/api/v1/misc/markup_test.go | 100 ++++++++++++++++++-----------
 routers/common/markup.go           |  63 +++++++++---------
 templates/swagger/v1_json.tmpl     |   4 +-
 5 files changed, 103 insertions(+), 78 deletions(-)

diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 66e8cf611d..5eb568cb1f 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -86,10 +86,10 @@ type RenderContext struct {
 }
 
 type Links struct {
-	AbsolutePrefix bool
-	Base           string
-	BranchPath     string
-	TreePath       string
+	AbsolutePrefix bool   // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
+	Base           string // base prefix for pre-provided links and medias (images, videos)
+	BranchPath     string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
+	TreePath       string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
 }
 
 func (l *Links) Prefix() string {
diff --git a/modules/structs/miscellaneous.go b/modules/structs/miscellaneous.go
index bff10f95b7..3b206c1dd7 100644
--- a/modules/structs/miscellaneous.go
+++ b/modules/structs/miscellaneous.go
@@ -25,7 +25,8 @@ type MarkupOption struct {
 	//
 	// in: body
 	Mode string
-	// Context to render
+	// URL path for rendering issue, media and file links
+	// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
 	//
 	// in: body
 	Context string
@@ -53,7 +54,8 @@ type MarkdownOption struct {
 	//
 	// in: body
 	Mode string
-	// Context to render
+	// URL path for rendering issue, media and file links
+	// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
 	//
 	// in: body
 	Context string
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 5236fd06ae..e2ab7141b7 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -7,6 +7,7 @@ import (
 	go_context "context"
 	"io"
 	"net/http"
+	"path"
 	"strings"
 	"testing"
 
@@ -19,36 +20,40 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-const (
-	AppURL  = "http://localhost:3000/"
-	Repo    = "gogits/gogs"
-	FullURL = AppURL + Repo + "/"
-)
+const AppURL = "http://localhost:3000/"
 
-func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
+func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
 	setting.AppURL = AppURL
+	context := "/gogits/gogs"
+	if !wiki {
+		context += path.Join("/src/branch/main", path.Dir(filePath))
+	}
 	options := api.MarkupOption{
 		Mode:     mode,
 		Text:     text,
-		Context:  Repo,
-		Wiki:     true,
+		Context:  context,
+		Wiki:     wiki,
 		FilePath: filePath,
 	}
 	ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
 	web.SetForm(ctx, &options)
 	Markup(ctx)
-	assert.Equal(t, responseBody, resp.Body.String())
-	assert.Equal(t, responseCode, resp.Code)
+	assert.Equal(t, expectedBody, resp.Body.String())
+	assert.Equal(t, expectedCode, resp.Code)
 	resp.Body.Reset()
 }
 
-func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) {
+func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
 	setting.AppURL = AppURL
+	context := "/gogits/gogs"
+	if !wiki {
+		context += "/src/branch/main"
+	}
 	options := api.MarkdownOption{
 		Mode:    mode,
 		Text:    text,
-		Context: Repo,
-		Wiki:    true,
+		Context: context,
+		Wiki:    wiki,
 	}
 	ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
 	web.SetForm(ctx, &options)
@@ -65,7 +70,7 @@ func TestAPI_RenderGFM(t *testing.T) {
 		},
 	})
 
-	testCasesCommon := []string{
+	testCasesWiki := []string{
 		// dear imgui wiki markdown extract: special wiki syntax
 		`Wiki! Enjoy :)
 - [[Links, Language bindings, Engine bindings|Links]]
@@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) {
 		// rendered
 		`<p>Wiki! Enjoy :)</p>
 <ul>
-<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
-<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
+<li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
+<li><a href="http://localhost:3000/gogits/gogs/wiki/Tips" rel="nofollow">Tips</a></li>
+<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
 </ul>
 `,
 		// Guard wiki sidebar: special syntax
 		`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
 		// rendered
-		`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+		`<p><a href="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
 `,
 		// special syntax
 		`[[Name|Link]]`,
 		// rendered
-		`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
+		`<p><a href="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p>
 `,
 		// empty
 		``,
@@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) {
 		``,
 	}
 
-	testCasesDocument := []string{
+	testCasesWikiDocument := []string{
 		// wine-staging wiki home extract: special wiki syntax, images
 		`## What is Wine Staging?
 **Wine Staging** on website [wine-staging.com](http://wine-staging.com).
@@ -111,31 +116,48 @@ Here are some links to the most important topics. You can find the full list of
 <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
 <h2 id="user-content-quick-links">Quick Links</h2>
 <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
-<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
-<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
+<p><a href="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a>
+<a href="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
 `,
 	}
 
-	for i := 0; i < len(testCasesCommon); i += 2 {
-		text := testCasesCommon[i]
-		response := testCasesCommon[i+1]
-		testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
-		testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
-		testRenderMarkdown(t, "comment", text, response, http.StatusOK)
-		testRenderMarkup(t, "comment", "", text, response, http.StatusOK)
-		testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
+	for i := 0; i < len(testCasesWiki); i += 2 {
+		text := testCasesWiki[i]
+		response := testCasesWiki[i+1]
+		testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
+		testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
+		testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
+		testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
+		testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
 	}
 
-	for i := 0; i < len(testCasesDocument); i += 2 {
-		text := testCasesDocument[i]
-		response := testCasesDocument[i+1]
-		testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
-		testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
-		testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
+	for i := 0; i < len(testCasesWikiDocument); i += 2 {
+		text := testCasesWikiDocument[i]
+		response := testCasesWikiDocument[i+1]
+		testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
+		testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
+		testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
 	}
 
-	testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
-	testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
+	input := "[Link](test.md)\n![Image](image.png)"
+	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
+<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
+`, http.StatusOK)
+
+	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
+<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
+`, http.StatusOK)
+
+	testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
+<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
+`, http.StatusOK)
+
+	testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/path/test.md" rel="nofollow">Link</a>
+<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
+`, http.StatusOK)
+
+	testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
+	testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
 }
 
 var simpleCases = []string{
@@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) {
 	options := api.MarkdownOption{
 		Mode:    "markdown",
 		Text:    "",
-		Context: Repo,
+		Context: "/gogits/gogs",
 	}
 	ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
 	for i := 0; i < len(simpleCases); i += 2 {
diff --git a/routers/common/markup.go b/routers/common/markup.go
index f7d096008a..0a00eac7d4 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -7,63 +7,67 @@ package common
 import (
 	"fmt"
 	"net/http"
+	"path"
 	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/httplib"
 	"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"
 	"code.gitea.io/gitea/services/context"
-
-	"mvdan.cc/xurls/v2"
 )
 
 // RenderMarkup renders markup text for the /markup and /markdown endpoints
-func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) {
-	var markupType string
-	relativePath := ""
+func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
+	// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
+	// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
+	// filePath will be used as RenderContext.RelativePath
 
-	if len(text) == 0 {
-		_, _ = ctx.Write([]byte(""))
-		return
+	// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
+	// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
+
+	var markupType, relativePath string
+
+	links := markup.Links{AbsolutePrefix: true}
+	if urlPathContext != "" {
+		links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
 	}
 
 	switch mode {
 	case "markdown":
 		// Raw markdown
 		if err := markdown.RenderRaw(&markup.RenderContext{
-			Ctx: ctx,
-			Links: markup.Links{
-				AbsolutePrefix: true,
-				Base:           urlPrefix,
-			},
+			Ctx:   ctx,
+			Links: links,
 		}, strings.NewReader(text), ctx.Resp); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 		}
 		return
 	case "comment":
-		// Comment as markdown
+		// Issue & comment content
 		markupType = markdown.MarkupName
 	case "gfm":
-		// Github Flavored Markdown as document
+		// GitHub Flavored Markdown
 		markupType = markdown.MarkupName
 	case "file":
-		// File as document based on file extension
-		markupType = ""
+		markupType = "" // render the repo file content by its extension
 		relativePath = filePath
 	default:
 		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
 		return
 	}
 
-	if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
-		// check if urlPrefix is already set to a URL
-		linkRegex, _ := xurls.StrictMatchingScheme("https?://")
-		m := linkRegex.FindStringIndex(urlPrefix)
-		if m == nil {
-			urlPrefix = util.URLJoin(setting.AppURL, urlPrefix)
-		}
+	fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5)
+	if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
+		// absolute base prefix is something like "https://host/subpath/{user}/{repo}"
+		absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1])
+
+		fileDir := path.Dir(filePath)                      // it is "doc" if filePath is "doc/CHANGE.md"
+		refPath := strings.Join(fields[3:], "/")           // it is "branch/features/feat-12/doc"
+		refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12"
+
+		links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
 	}
 
 	meta := map[string]string{}
@@ -81,12 +85,9 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	}
 
 	if err := markup.Render(&markup.RenderContext{
-		Ctx:  ctx,
-		Repo: repoCtx,
-		Links: markup.Links{
-			AbsolutePrefix: true,
-			Base:           urlPrefix,
-		},
+		Ctx:          ctx,
+		Repo:         repoCtx,
+		Links:        links,
 		Metas:        meta,
 		IsWiki:       wiki,
 		Type:         markupType,
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index ebfdcb6a8f..4aa64c5376 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -22404,7 +22404,7 @@
       "type": "object",
       "properties": {
         "Context": {
-          "description": "Context to render\n\nin: body",
+          "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body",
           "type": "string"
         },
         "Mode": {
@@ -22427,7 +22427,7 @@
       "type": "object",
       "properties": {
         "Context": {
-          "description": "Context to render\n\nin: body",
+          "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body",
           "type": "string"
         },
         "FilePath": {