// Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package markup_test import ( "context" "io" "os" "strings" "testing" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "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/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var localMetas = map[string]string{ "user": "gogits", "repo": "gogs", "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", } func TestMain(m *testing.M) { unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } os.Exit(m.Run()) } func TestRender_Commits(t *testing.T) { setting.AppURL = markup.TestAppURL test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: ".md", Links: markup.Links{ AbsolutePrefix: true, Base: markup.TestRepoURL, }, Metas: localMetas, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" repo := markup.TestRepoURL commit := util.URLJoin(repo, "commit", sha) tree := util.URLJoin(repo, "tree", sha, "src") file := util.URLJoin(repo, "commit", sha, "example.txt") fileWithExtra := file + ":" fileWithHash := file + "#L2" fileWithHasExtra := file + "#L2:" commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha) commitCompareWithHash := commitCompare + "#L2" test(sha, `

65f1bf27bc

`) test(sha[:7], `

65f1bf2

`) test(sha[:39], `

65f1bf27bc

`) test(commit, `

65f1bf27bc

`) test(tree, `

65f1bf27bc/src

`) test(file, `

65f1bf27bc/example.txt

`) test(fileWithExtra, `

65f1bf27bc/example.txt:

`) test(fileWithHash, `

65f1bf27bc/example.txt (L2)

`) test(fileWithHasExtra, `

65f1bf27bc/example.txt (L2):

`) test(commitCompare, `

65f1bf27bc...65f1bf27bc

`) test(commitCompareWithHash, `

65f1bf27bc...65f1bf27bc (L2)

`) test("commit "+sha, `

commit 65f1bf27bc

`) test("/home/gitea/"+sha, "

/home/gitea/"+sha+"

") test("deadbeef", `

deadbeef

`) test("d27ace93", `

d27ace93

`) test(sha[:14]+".x", `

`+sha[:14]+`.x

`) expected14 := `` + sha[:10] + `` test(sha[:14]+".", `

`+expected14+`.

`) test(sha[:14]+",", `

`+expected14+`,

`) test("["+sha[:14]+"]", `

[`+expected14+`]

`) } func TestRender_CrossReferences(t *testing.T) { setting.AppURL = markup.TestAppURL test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: "a.md", Links: markup.Links{ AbsolutePrefix: true, Base: setting.AppSubURL, }, Metas: localMetas, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } test( "gogits/gogs#12345", `

gogits/gogs#12345

`) test( "go-gitea/gitea#12345", `

go-gitea/gitea#12345

`) test( "/home/gitea/go-gitea/gitea#12345", `

/home/gitea/go-gitea/gitea#12345

`) test( util.URLJoin(markup.TestAppURL, "gogitea", "gitea", "issues", "12345"), `

gogitea/gitea#12345

`) test( util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345"), `

go-gitea/gitea#12345

`) test( util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), `

gogitea/some-repo-name#12345

`) } func TestMisc_IsSameDomain(t *testing.T) { setting.AppURL = markup.TestAppURL sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" commit := util.URLJoin(markup.TestRepoURL, "commit", sha) assert.True(t, markup.IsSameDomain(commit)) assert.False(t, markup.IsSameDomain("http://google.com/ncr")) assert.False(t, markup.IsSameDomain("favicon.ico")) } func TestRender_links(t *testing.T) { setting.AppURL = markup.TestAppURL test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: "a.md", Links: markup.Links{ Base: markup.TestRepoURL, }, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } // Text that should be turned into URL defaultCustom := setting.Markdown.CustomURLSchemes setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"} markup.InitializeSanitizer() markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) test( "https://www.example.com", `

https://www.example.com

`) test( "http://www.example.com", `

http://www.example.com

`) test( "https://example.com", `

https://example.com

`) test( "http://example.com", `

http://example.com

`) test( "http://foo.com/blah_blah", `

http://foo.com/blah_blah

`) test( "http://foo.com/blah_blah/", `

http://foo.com/blah_blah/

`) test( "http://www.example.com/wpstyle/?p=364", `

http://www.example.com/wpstyle/?p=364

`) test( "https://www.example.com/foo/?bar=baz&inga=42&quux", `

https://www.example.com/foo/?bar=baz&inga=42&quux

`) test( "http://142.42.1.1/", `

http://142.42.1.1/

`) test( "https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd", `

https://github.com/go-gitea/gitea/?p=aaa/bbb.html#ccc-ddd

`) test( "https://en.wikipedia.org/wiki/URL_(disambiguation)", `

https://en.wikipedia.org/wiki/URL_(disambiguation)

`) test( "https://foo_bar.example.com/", `

https://foo_bar.example.com/

`) test( "https://stackoverflow.com/questions/2896191/what-is-go-used-fore", `

https://stackoverflow.com/questions/2896191/what-is-go-used-fore

`) test( "https://username:password@gitea.com", `

https://username:password@gitea.com

`) test( "ftp://gitea.com/file.txt", `

ftp://gitea.com/file.txt

`) test( "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download", `

magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download

`) // Test that should *not* be turned into URL test( "www.example.com", `

www.example.com

`) test( "example.com", `

example.com

`) test( "test.example.com", `

test.example.com

`) test( "http://", `

http://

`) test( "https://", `

https://

`) test( "://", `

://

`) test( "www", `

www

`) test( "ftps://gitea.com", `

ftps://gitea.com

`) // Restore previous settings setting.Markdown.CustomURLSchemes = defaultCustom markup.InitializeSanitizer() markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) } func TestRender_email(t *testing.T) { setting.AppURL = markup.TestAppURL test := func(input, expected string) { res, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: "a.md", Links: markup.Links{ Base: markup.TestRepoURL, }, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) } // Text that should be turned into email link test( "info@gitea.com", `

info@gitea.com

`) test( "(info@gitea.com)", `

(info@gitea.com)

`) test( "[info@gitea.com]", `

[info@gitea.com]

`) test( "info@gitea.com.", `

info@gitea.com.

`) test( "firstname+lastname@gitea.com", `

firstname+lastname@gitea.com

`) test( "send email to info@gitea.co.uk.", `

send email to info@gitea.co.uk.

`) test( `j.doe@example.com, j.doe@example.com. j.doe@example.com; j.doe@example.com? j.doe@example.com!`, `

j.doe@example.com,
j.doe@example.com.
j.doe@example.com;
j.doe@example.com?
j.doe@example.com!

`) // Test that should *not* be turned into email links test( "\"info@gitea.com\"", `

"info@gitea.com"

`) test( "/home/gitea/mailstore/info@gitea/com", `

/home/gitea/mailstore/info@gitea/com

`) test( "git@try.gitea.io:go-gitea/gitea.git", `

git@try.gitea.io:go-gitea/gitea.git

`) test( "gitea@3", `

gitea@3

`) test( "gitea@gmail.c", `

gitea@gmail.c

`) test( "email@domain@domain.com", `

email@domain@domain.com

`) test( "email@domain..com", `

email@domain..com

`) } func TestRender_emoji(t *testing.T) { setting.AppURL = markup.TestAppURL setting.StaticURLPrefix = markup.TestAppURL test := func(input, expected string) { expected = strings.ReplaceAll(expected, "&", "&") buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: "a.md", Links: markup.Links{ Base: markup.TestRepoURL, }, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } // Make sure we can successfully match every emoji in our dataset with regex for i := range emoji.GemojiData { test( emoji.GemojiData[i].Emoji, `

`+emoji.GemojiData[i].Emoji+`

`) } for i := range emoji.GemojiData { test( ":"+emoji.GemojiData[i].Aliases[0]+":", `

`+emoji.GemojiData[i].Emoji+`

`) } // Text that should be turned into or recognized as emoji test( ":gitea:", `

:gitea:

`) test( ":custom-emoji:", `

:custom-emoji:

`) setting.UI.CustomEmojisMap["custom-emoji"] = ":custom-emoji:" test( ":custom-emoji:", `

:custom-emoji:

`) test( "θΏ™ζ˜―ε­—η¬¦:1::+1: some🐊 \U0001f44d:custom-emoji: :gitea:", `

θΏ™ζ˜―ε­—η¬¦:1:πŸ‘ some🐊 `+ `πŸ‘:custom-emoji: `+ `:gitea:

`) test( "Some text with πŸ˜„ in the middle", `

Some text with πŸ˜„ in the middle

`) test( "Some text with :smile: in the middle", `

Some text with πŸ˜„ in the middle

`) test( "Some text with πŸ˜„πŸ˜„ 2 emoji next to each other", `

Some text with πŸ˜„πŸ˜„ 2 emoji next to each other

`) test( "😎πŸ€ͺπŸ”πŸ€‘β“", `

😎πŸ€ͺπŸ”πŸ€‘β“

`) // should match nothing test( "2001:0db8:85a3:0000:0000:8a2e:0370:7334", `

2001:0db8:85a3:0000:0000:8a2e:0370:7334

`) test( ":not exist:", `

:not exist:

`) } func TestRender_ShortLinks(t *testing.T) { setting.AppURL = markup.TestAppURL tree := util.URLJoin(markup.TestRepoURL, "src", "master") test := func(input, expected, expectedWiki string) { buffer, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: markup.TestRepoURL, BranchPath: "master", }, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: markup.TestRepoURL, }, Metas: localMetas, IsWiki: true, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } mediatree := util.URLJoin(markup.TestRepoURL, "media", "master") url := util.URLJoin(tree, "Link") otherURL := util.URLJoin(tree, "Other-Link") encodedURL := util.URLJoin(tree, "Link%3F") imgurl := util.URLJoin(mediatree, "Link.jpg") otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg") encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg") notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg") urlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link") otherURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Other-Link") encodedURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link%3F") imgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link.jpg") otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg") encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg") notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg") favicon := "http://google.com/favicon.ico" test( "[[Link]]", `

Link

`, `

Link

`) test( "[[Link.jpg]]", `

Link.jpg

`, `

Link.jpg

`) test( "[["+favicon+"]]", `

`+favicon+`

`, `

`+favicon+`

`) test( "[[Name|Link]]", `

Name

`, `

Name

`) test( "[[Name|Link.jpg]]", `

Name

`, `

Name

`) test( "[[Name|Link.jpg|alt=AltName]]", `

AltName

`, `

AltName

`) test( "[[Name|Link.jpg|title=Title]]", `

Title

`, `

Title

`) test( "[[Name|Link.jpg|alt=AltName|title=Title]]", `

AltName

`, `

AltName

`) test( "[[Name|Link.jpg|alt=\"AltName\"|title='Title']]", `

AltName

`, `

AltName

`) test( "[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]", `

AltName

`, `

AltName

`) test( "[[Link]] [[Other Link]]", `

Link Other Link

`, `

Link Other Link

`) test( "[[Link?]]", `

Link?

`, `

Link?

`) test( "[[Link]] [[Other Link]] [[Link?]]", `

Link Other Link Link?

`, `

Link Other Link Link?

`) test( "[[Link #.jpg]]", `

Link #.jpg

`, `

Link #.jpg

`) test( "[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]", `

AltName

`, `

AltName

`) test( "[[some/path/Link #.jpg]]", `

some/path/Link #.jpg

`, `

some/path/Link #.jpg

`) test( "

[[foobar]]

", `

[[foobar]]

`, `

[[foobar]]

`) } func TestRender_RelativeImages(t *testing.T) { setting.AppURL = markup.TestAppURL test := func(input, expected, expectedWiki string) { buffer, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: markup.TestRepoURL, BranchPath: "master", }, Metas: localMetas, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: markup.TestRepoURL, }, Metas: localMetas, IsWiki: true, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw") mediatree := util.URLJoin(markup.TestRepoURL, "media", "master") test( ``, ``, ``) test( ``, ``, ``) } func Test_ParseClusterFuzz(t *testing.T) { setting.AppURL = markup.TestAppURL localMetas := map[string]string{ "user": "go-gitea", "repo": "gitea", } data := "
go-gitea/gitea#12345`) // Test that other post processing still works. test( ":gitea:", `:gitea:`) test( "Some text with πŸ˜„ in the middle", `Some text with πŸ˜„ in the middle`) test("http://localhost:3000/person/repo/issues/4#issuecomment-1234", `person/repo#4 (comment)`) } func TestIssue16020(t *testing.T) { setting.AppURL = markup.TestAppURL localMetas := map[string]string{ "user": "go-gitea", "repo": "gitea", } data := `` var res strings.Builder err := markup.PostProcess(&markup.RenderContext{ Ctx: git.DefaultContext, Metas: localMetas, }, strings.NewReader(data), &res) require.NoError(t, err) assert.Equal(t, data, res.String()) } func BenchmarkEmojiPostprocess(b *testing.B) { data := "πŸ₯° " for len(data) < 1<<16 { data += data } b.ResetTimer() for i := 0; i < b.N; i++ { var res strings.Builder err := markup.PostProcess(&markup.RenderContext{ Ctx: git.DefaultContext, Metas: localMetas, }, strings.NewReader(data), &res) require.NoError(b, err) } } func TestFuzz(t *testing.T) { s := "t/l/issues/8#/../../a" renderContext := markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: "https://example.com/go-gitea/gitea", }, Metas: map[string]string{ "user": "go-gitea", "repo": "gitea", }, } err := markup.PostProcess(&renderContext, strings.NewReader(s), io.Discard) require.NoError(t, err) } func TestIssue18471(t *testing.T) { data := `http://domain/org/repo/compare/783b039...da951ce` var res strings.Builder err := markup.PostProcess(&markup.RenderContext{ Ctx: git.DefaultContext, Metas: localMetas, }, strings.NewReader(data), &res) require.NoError(t, err) assert.Equal(t, "783b039...da951ce", res.String()) } func TestRender_FilePreview(t *testing.T) { defer test.MockVariableValue(&setting.StaticRootPath, "../../")() defer test.MockVariableValue(&setting.Names, []string{"english"})() defer test.MockVariableValue(&setting.Langs, []string{"en-US"})() translation.InitLocales(context.Background()) setting.AppURL = markup.TestAppURL markup.Init(&markup.ProcessorHelper{ GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { gitRepo, err := git.OpenRepository(git.DefaultContext, "./tests/repo/repo1_filepreview") require.NoError(t, err) defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") require.NoError(t, err) blob, err := commit.GetBlobByPath("path/to/file.go") require.NoError(t, err) return blob, nil }, }) sha := "190d9492934af498c3f669d6a2431dc5459e5b20" commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3" testRender := func(input, expected string, metas map[string]string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, RelativePath: ".md", Metas: metas, }, input) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } t.Run("single", func(t *testing.T) { testRender( commitFilePreview, `

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, localMetas, ) }) t.Run("cross-repo", func(t *testing.T) { testRender( commitFilePreview, `

`+ `
`+ `
`+ `
`+ `gogits/gogs – `+ `path/to/file.go`+ `
`+ ``+ `Lines 2 to 3 in gogits/gogs@190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, map[string]string{ "user": "gogits", "repo": "gogs2", }, ) }) t.Run("AppSubURL", func(t *testing.T) { urlWithSub := util.URLJoin(markup.TestAppURL, "sub", markup.TestOrgRepo, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3" testRender( urlWithSub, `

190d949293/path/to/file.go (L2-L3)

`, localMetas, ) defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL+"sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() testRender( urlWithSub, `

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, localMetas, ) testRender( "first without sub "+commitFilePreview+" second "+urlWithSub, `

first without sub 190d949293/path/to/file.go (L2-L3) second

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, localMetas, ) }) t.Run("multiples", func(t *testing.T) { testRender( "first "+commitFilePreview+" second "+commitFilePreview, `

first

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

second

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, localMetas, ) testRender( "first "+commitFilePreview+" second "+commitFilePreview+" third "+commitFilePreview, `

first

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

second

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

third

`+ `
`+ `
`+ ``+ ``+ `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
B`+"\n"+`
C`+"\n"+`
`+ `
`+ `
`+ `

`, localMetas, ) }) }