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>
This commit is contained in:
Brecht Van Lommel 2024-06-17 08:16:14 +02:00 committed by GitHub
parent f5dfd7d73c
commit 597d1da96b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 103 additions and 78 deletions

View File

@ -86,10 +86,10 @@ type RenderContext struct {
} }
type Links struct { type Links struct {
AbsolutePrefix bool AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
Base string Base string // base prefix for pre-provided links and medias (images, videos)
BranchPath string BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
TreePath string TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
} }
func (l *Links) Prefix() string { func (l *Links) Prefix() string {

View File

@ -25,7 +25,8 @@ type MarkupOption struct {
// //
// in: body // in: body
Mode string 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 // in: body
Context string Context string
@ -53,7 +54,8 @@ type MarkdownOption struct {
// //
// in: body // in: body
Mode string 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 // in: body
Context string Context string

View File

@ -7,6 +7,7 @@ import (
go_context "context" go_context "context"
"io" "io"
"net/http" "net/http"
"path"
"strings" "strings"
"testing" "testing"
@ -19,36 +20,40 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const ( const AppURL = "http://localhost:3000/"
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
FullURL = AppURL + Repo + "/"
)
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 setting.AppURL = AppURL
context := "/gogits/gogs"
if !wiki {
context += path.Join("/src/branch/main", path.Dir(filePath))
}
options := api.MarkupOption{ options := api.MarkupOption{
Mode: mode, Mode: mode,
Text: text, Text: text,
Context: Repo, Context: context,
Wiki: true, Wiki: wiki,
FilePath: filePath, FilePath: filePath,
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
web.SetForm(ctx, &options) web.SetForm(ctx, &options)
Markup(ctx) Markup(ctx)
assert.Equal(t, responseBody, resp.Body.String()) assert.Equal(t, expectedBody, resp.Body.String())
assert.Equal(t, responseCode, resp.Code) assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset() 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 setting.AppURL = AppURL
context := "/gogits/gogs"
if !wiki {
context += "/src/branch/main"
}
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: mode, Mode: mode,
Text: text, Text: text,
Context: Repo, Context: context,
Wiki: true, Wiki: wiki,
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options) 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 // dear imgui wiki markdown extract: special wiki syntax
`Wiki! Enjoy :) `Wiki! Enjoy :)
- [[Links, Language bindings, Engine bindings|Links]] - [[Links, Language bindings, Engine bindings|Links]]
@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) {
// rendered // rendered
`<p>Wiki! Enjoy :)</p> `<p>Wiki! Enjoy :)</p>
<ul> <ul>
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> <li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li> <li><a href="http://localhost:3000/gogits/gogs/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>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> </ul>
`, `,
// Guard wiki sidebar: special syntax // Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered // 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 // special syntax
`[[Name|Link]]`, `[[Name|Link]]`,
// rendered // 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 // empty
``, ``,
@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) {
``, ``,
} }
testCasesDocument := []string{ testCasesWikiDocument := []string{
// wine-staging wiki home extract: special wiki syntax, images // wine-staging wiki home extract: special wiki syntax, images
`## What is Wine Staging? `## What is Wine Staging?
**Wine Staging** on website [wine-staging.com](http://wine-staging.com). **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> <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> <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>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> <p><a href="http://localhost:3000/gogits/gogs/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> <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 { for i := 0; i < len(testCasesWiki); i += 2 {
text := testCasesCommon[i] text := testCasesWiki[i]
response := testCasesCommon[i+1] response := testCasesWiki[i+1]
testRenderMarkdown(t, "gfm", text, response, http.StatusOK) testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkdown(t, "comment", text, response, http.StatusOK) testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
testRenderMarkup(t, "comment", "", text, response, http.StatusOK) testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
} }
for i := 0; i < len(testCasesDocument); i += 2 { for i := 0; i < len(testCasesWikiDocument); i += 2 {
text := testCasesDocument[i] text := testCasesWikiDocument[i]
response := testCasesDocument[i+1] response := testCasesWikiDocument[i+1]
testRenderMarkdown(t, "gfm", text, response, http.StatusOK) testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", "path/test.md", 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) input := "[Link](test.md)\n![Image](image.png)"
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) 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{ var simpleCases = []string{
@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) {
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: "markdown", Mode: "markdown",
Text: "", Text: "",
Context: Repo, Context: "/gogits/gogs",
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 { for i := 0; i < len(simpleCases); i += 2 {

View File

@ -7,63 +7,67 @@ package common
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
repo_model "code.gitea.io/gitea/models/repo" 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"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"mvdan.cc/xurls/v2"
) )
// RenderMarkup renders markup text for the /markup and /markdown endpoints // 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) { func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
var markupType string // urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
relativePath := "" // 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 { // for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
_, _ = ctx.Write([]byte("")) // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
return
var markupType, relativePath string
links := markup.Links{AbsolutePrefix: true}
if urlPathContext != "" {
links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
} }
switch mode { switch mode {
case "markdown": case "markdown":
// Raw markdown // Raw markdown
if err := markdown.RenderRaw(&markup.RenderContext{ if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Links: markup.Links{ Links: links,
AbsolutePrefix: true,
Base: urlPrefix,
},
}, strings.NewReader(text), ctx.Resp); err != nil { }, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
} }
return return
case "comment": case "comment":
// Comment as markdown // Issue & comment content
markupType = markdown.MarkupName markupType = markdown.MarkupName
case "gfm": case "gfm":
// Github Flavored Markdown as document // GitHub Flavored Markdown
markupType = markdown.MarkupName markupType = markdown.MarkupName
case "file": case "file":
// File as document based on file extension markupType = "" // render the repo file content by its extension
markupType = ""
relativePath = filePath relativePath = filePath
default: default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
return return
} }
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5)
// check if urlPrefix is already set to a URL if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
linkRegex, _ := xurls.StrictMatchingScheme("https?://") // absolute base prefix is something like "https://host/subpath/{user}/{repo}"
m := linkRegex.FindStringIndex(urlPrefix) absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1])
if m == nil {
urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) 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{} 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{ if err := markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Repo: repoCtx, Repo: repoCtx,
Links: markup.Links{ Links: links,
AbsolutePrefix: true,
Base: urlPrefix,
},
Metas: meta, Metas: meta,
IsWiki: wiki, IsWiki: wiki,
Type: markupType, Type: markupType,

View File

@ -22404,7 +22404,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"Context": { "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" "type": "string"
}, },
"Mode": { "Mode": {
@ -22427,7 +22427,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"Context": { "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" "type": "string"
}, },
"FilePath": { "FilePath": {