Merge branch 'main' into lunny/remove_num_watches

This commit is contained in:
Lunny Xiao 2024-11-26 20:56:55 -08:00
commit 6f1b0f762e
154 changed files with 3576 additions and 2101 deletions

View File

@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules lint-js: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
# npx tsc # npx vue-tsc
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules lint-js-fix: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
# npx tsc # npx vue-tsc
.PHONY: lint-css .PHONY: lint-css
lint-css: node_modules lint-css: node_modules
@ -451,6 +451,10 @@ lint-templates: .venv node_modules
lint-yaml: .venv lint-yaml: .venv
@poetry run yamllint . @poetry run yamllint .
.PHONY: tsc
tsc:
npx vue-tsc
.PHONY: watch .PHONY: watch
watch: watch:
@bash tools/watch.sh @bash tools/watch.sh

View File

@ -1090,8 +1090,8 @@
"licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" "licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
}, },
{ {
"name": "github.com/stretchr/testify/assert", "name": "github.com/stretchr/testify",
"path": "github.com/stretchr/testify/assert/LICENSE", "path": "github.com/stretchr/testify/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" "licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
}, },
{ {

View File

@ -200,7 +200,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
} }
} }
func (a *Action) loadRepo(ctx context.Context) { func (a *Action) LoadRepo(ctx context.Context) {
if a.Repo != nil { if a.Repo != nil {
return return
} }
@ -250,7 +250,7 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
// GetRepoUserName returns the name of the action repository owner. // GetRepoUserName returns the name of the action repository owner.
func (a *Action) GetRepoUserName(ctx context.Context) string { func (a *Action) GetRepoUserName(ctx context.Context) string {
a.loadRepo(ctx) a.LoadRepo(ctx)
if a.Repo == nil { if a.Repo == nil {
return "(non-existing-repo)" return "(non-existing-repo)"
} }
@ -265,7 +265,7 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string {
// GetRepoName returns the name of the action repository. // GetRepoName returns the name of the action repository.
func (a *Action) GetRepoName(ctx context.Context) string { func (a *Action) GetRepoName(ctx context.Context) string {
a.loadRepo(ctx) a.LoadRepo(ctx)
if a.Repo == nil { if a.Repo == nil {
return "(non-existing-repo)" return "(non-existing-repo)"
} }
@ -644,7 +644,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
} }
if repoChanged { if repoChanged {
act.loadRepo(ctx) act.LoadRepo(ctx)
repo = act.Repo repo = act.Repo
// check repo owner exist. // check repo owner exist.
@ -770,7 +770,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64)
// CountActionCreatedUnixString count actions where created_unix is an empty string // CountActionCreatedUnixString count actions where created_unix is an empty string
func CountActionCreatedUnixString(ctx context.Context) (int64, error) { func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() { if setting.Database.Type.IsSQLite3() {
return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action))
} }
return 0, nil return 0, nil
} }
@ -778,7 +778,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
// FixActionCreatedUnixString set created_unix to zero if it is an empty string // FixActionCreatedUnixString set created_unix to zero if it is an empty string
func FixActionCreatedUnixString(ctx context.Context) (int64, error) { func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() { if setting.Database.Type.IsSQLite3() {
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -256,7 +256,7 @@ func TestConsistencyUpdateAction(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ID: int64(id), ID: int64(id),
}) })
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id) _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id)
assert.NoError(t, err) assert.NoError(t, err)
actions := make([]*activities_model.Action, 0, 1) actions := make([]*activities_model.Action, 0, 1)
// //

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
) )
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
// WebAuthnCredentialList is a list of *WebAuthnCredential // WebAuthnCredentialList is a list of *WebAuthnCredential
type WebAuthnCredentialList []*WebAuthnCredential type WebAuthnCredentialList []*WebAuthnCredential
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
return webauthn.CredentialFlags{
UserPresent: flags.HasUserPresent(),
UserVerified: flags.HasUserVerified(),
BackupEligible: flags.HasBackupEligible(),
BackupState: flags.HasBackupState(),
}
}
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials // ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential { func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
// TODO: at the moment, Gitea doesn't store or check the flags
// so we need to use the default flags from the authenticator to make the login validation pass
// In the future, we should:
// 1. store the flags when registering the credential
// 2. provide the stored flags when converting the credentials (for login)
// 3. for old users, still use this fallback to the default flags
defAuthFlags := util.OptionalArg(defaultAuthFlags)
creds := make([]webauthn.Credential, 0, len(list)) creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list { for _, cred := range list {
creds = append(creds, webauthn.Credential{ creds = append(creds, webauthn.Credential{
ID: cred.CredentialID, ID: cred.CredentialID,
PublicKey: cred.PublicKey, PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType, AttestationType: cred.AttestationType,
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
Authenticator: webauthn.Authenticator{ Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID, AAGUID: cred.AAGUID,
SignCount: cred.SignCount, SignCount: cred.SignCount,

View File

@ -134,6 +134,9 @@ func SyncAllTables() error {
func InitEngine(ctx context.Context) error { func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine() xormEngine, err := newXORMEngine()
if err != nil { if err != nil {
if strings.Contains(err.Error(), "SQLite3 support") {
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return fmt.Errorf("failed to connect to database: %w", err) return fmt.Errorf("failed to connect to database: %w", err)
} }

View File

@ -34,6 +34,7 @@ type ProtectedBranch struct {
RepoID int64 `xorm:"UNIQUE(s)"` RepoID int64 `xorm:"UNIQUE(s)"`
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
globRule glob.Glob `xorm:"-"` globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"` isPlainName bool `xorm:"-"`
CanPush bool `xorm:"NOT NULL DEFAULT false"` CanPush bool `xorm:"NOT NULL DEFAULT false"`
@ -413,14 +414,27 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
} }
protectBranch.ApprovalsWhitelistTeamIDs = whitelist protectBranch.ApprovalsWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists // Looks like it's a new rule
if protectBranch.ID == 0 { if protectBranch.ID == 0 {
// as it's a new rule and if priority was not set, we need to calc it.
if protectBranch.Priority == 0 {
var lowestPrio int64
// because of mssql we can not use builder or save xorm syntax, so raw sql it is
if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM protected_branch WHERE repo_id = ?`, protectBranch.RepoID).
Get(&lowestPrio); err != nil {
return err
}
log.Trace("Create new ProtectedBranch at repo[%d] and detect current lowest priority '%d'", protectBranch.RepoID, lowestPrio)
protectBranch.Priority = lowestPrio + 1
}
if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil { if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %v", err) return fmt.Errorf("Insert: %v", err)
} }
return nil return nil
} }
// update the rule
if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil { if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %v", err) return fmt.Errorf("Update: %v", err)
} }
@ -428,6 +442,24 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
return nil return nil
} }
func UpdateProtectBranchPriorities(ctx context.Context, repo *repo_model.Repository, ids []int64) error {
prio := int64(1)
return db.WithTx(ctx, func(ctx context.Context) error {
for _, id := range ids {
if _, err := db.GetEngine(ctx).
ID(id).Where("repo_id = ?", repo.ID).
Cols("priority").
Update(&ProtectedBranch{
Priority: prio,
}); err != nil {
return err
}
prio++
}
return nil
})
}
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with // updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo. // the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {

View File

@ -28,6 +28,13 @@ func (rules ProtectedBranchRules) sort() {
sort.Slice(rules, func(i, j int) bool { sort.Slice(rules, func(i, j int) bool {
rules[i].loadGlob() rules[i].loadGlob()
rules[j].loadGlob() rules[j].loadGlob()
// if priority differ, use that to sort
if rules[i].Priority != rules[j].Priority {
return rules[i].Priority < rules[j].Priority
}
// now we sort the old way
if rules[i].isPlainName != rules[j].isPlainName { if rules[i].isPlainName != rules[j].isPlainName {
return rules[i].isPlainName // plain name comes first, so plain name means "less" return rules[i].isPlainName // plain name comes first, so plain name means "less"
} }

View File

@ -75,7 +75,7 @@ func TestBranchRuleMatchPriority(t *testing.T) {
} }
} }
func TestBranchRuleSort(t *testing.T) { func TestBranchRuleSortLegacy(t *testing.T) {
in := []*ProtectedBranch{{ in := []*ProtectedBranch{{
RuleName: "b", RuleName: "b",
CreatedUnix: 1, CreatedUnix: 1,
@ -103,3 +103,37 @@ func TestBranchRuleSort(t *testing.T) {
} }
assert.Equal(t, expect, got) assert.Equal(t, expect, got)
} }
func TestBranchRuleSortPriority(t *testing.T) {
in := []*ProtectedBranch{{
RuleName: "b",
CreatedUnix: 1,
Priority: 4,
}, {
RuleName: "b/*",
CreatedUnix: 3,
Priority: 2,
}, {
RuleName: "a/*",
CreatedUnix: 2,
Priority: 1,
}, {
RuleName: "c",
CreatedUnix: 0,
Priority: 0,
}, {
RuleName: "a",
CreatedUnix: 4,
Priority: 3,
}}
expect := []string{"c", "a/*", "b/*", "a", "b"}
pbr := ProtectedBranchRules(in)
pbr.sort()
var got []string
for i := range pbr {
got = append(got, pbr[i].RuleName)
}
assert.Equal(t, expect, got)
}

View File

@ -7,6 +7,10 @@ import (
"fmt" "fmt"
"testing" "testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -76,3 +80,77 @@ func TestBranchRuleMatch(t *testing.T) {
) )
} }
} }
func TestUpdateProtectBranchPriorities(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create some test protected branches with initial priorities
protectedBranches := []*ProtectedBranch{
{
RepoID: repo.ID,
RuleName: "master",
Priority: 1,
},
{
RepoID: repo.ID,
RuleName: "develop",
Priority: 2,
},
{
RepoID: repo.ID,
RuleName: "feature/*",
Priority: 3,
},
}
for _, pb := range protectedBranches {
_, err := db.GetEngine(db.DefaultContext).Insert(pb)
assert.NoError(t, err)
}
// Test updating priorities
newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID}
err := UpdateProtectBranchPriorities(db.DefaultContext, repo, newPriorities)
assert.NoError(t, err)
// Verify new priorities
pbs, err := FindRepoProtectedBranchRules(db.DefaultContext, repo.ID)
assert.NoError(t, err)
expectedPriorities := map[string]int64{
"feature/*": 1,
"master": 2,
"develop": 3,
}
for _, pb := range pbs {
assert.Equal(t, expectedPriorities[pb.RuleName], pb.Priority)
}
}
func TestNewProtectBranchPriority(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
err := UpdateProtectBranch(db.DefaultContext, repo, &ProtectedBranch{
RepoID: repo.ID,
RuleName: "branch-1",
Priority: 1,
}, WhitelistOptions{})
assert.NoError(t, err)
newPB := &ProtectedBranch{
RepoID: repo.ID,
RuleName: "branch-2",
// Priority intentionally omitted
}
err = UpdateProtectBranch(db.DefaultContext, repo, newPB, WhitelistOptions{})
assert.NoError(t, err)
savedPB2, err := GetFirstMatchProtectedBranchRule(db.DefaultContext, repo.ID, "branch-2")
assert.NoError(t, err)
assert.Equal(t, int64(2), savedPB2.Priority)
}

View File

@ -1108,7 +1108,7 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList,
sess.Join("INNER", "issue", "issue.id = comment.issue_id") sess.Join("INNER", "issue", "issue.id = comment.issue_id")
} }
if opts.Page != 0 { if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts) sess = db.SetSessionPagination(sess, opts)
} }

View File

@ -7,8 +7,8 @@ import (
"context" "context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"xorm.io/builder" "xorm.io/builder"
@ -112,12 +112,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
} }
var err error var err error
rctx := markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
WithRepoFacade(issue.Repo). if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
WithLinks(markup.Links{Base: issue.Repo.Link()}).
WithMetas(issue.Repo.ComposeMetas(ctx))
if comment.RenderedContent, err = markdown.RenderString(rctx,
comment.Content); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -641,7 +641,7 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
Where("issue_id = ?", issue.ID). Where("issue_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list // sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID) OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
if opts.Page != 0 { if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)
} }
err = sess.Find(&issueDeps) err = sess.Find(&issueDeps)

View File

@ -105,7 +105,7 @@ func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOpt
And("`user`.prohibit_login = ?", false). And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id") Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
watches := make([]*IssueWatch, 0, listOptions.PageSize) watches := make([]*IssueWatch, 0, listOptions.PageSize)
return watches, sess.Find(&watches) return watches, sess.Find(&watches)

View File

@ -390,7 +390,7 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
sess.Asc("name") sess.Asc("name")
} }
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
} }
@ -462,7 +462,7 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
sess.Asc("name") sess.Asc("name")
} }
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
} }

View File

@ -406,7 +406,7 @@ func TestDeleteIssueLabel(t *testing.T) {
PosterID: doerID, PosterID: doerID,
IssueID: issueID, IssueID: issueID,
LabelID: labelID, LabelID: labelID,
}, `content=""`) }, `content=''`)
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}) label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
assert.EqualValues(t, expectedNumIssues, label.NumIssues) assert.EqualValues(t, expectedNumIssues, label.NumIssues)
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues) assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)

View File

@ -163,7 +163,7 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
Where(opts.toConds()). Where(opts.toConds()).
In("reaction.`type`", setting.UI.Reactions). In("reaction.`type`", setting.UI.Reactions).
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id") Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
if opts.Page != 0 { if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)
reactions := make([]*Reaction, 0, opts.PageSize) reactions := make([]*Reaction, 0, opts.PageSize)

View File

@ -96,7 +96,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) { func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
sws := make([]*Stopwatch, 0, 8) sws := make([]*Stopwatch, 0, 8)
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID) sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
} }

View File

@ -139,7 +139,7 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
sess = sess.Where(opts.ToConds()) sess = sess.Where(opts.ToConds())
if opts.Page != 0 { if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts) sess = db.SetSessionPagination(sess, opts)
} }

View File

@ -18,7 +18,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/testlogger"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -33,15 +33,15 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
ourSkip := 2 ourSkip := 2
ourSkip += skip ourSkip += skip
deferFn := testlogger.PrintCurrentTest(t, ourSkip) deferFn := testlogger.PrintCurrentTest(t, ourSkip)
assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
if err := deleteDB(); err != nil { if err := deleteDB(); err != nil {
t.Errorf("unable to reset database: %v", err) t.Fatalf("unable to reset database: %v", err)
return nil, deferFn return nil, deferFn
} }
x, err := newXORMEngine() x, err := newXORMEngine()
assert.NoError(t, err) require.NoError(t, err)
if x != nil { if x != nil {
oldDefer := deferFn oldDefer := deferFn
deferFn = func() { deferFn = func() {

View File

@ -367,6 +367,7 @@ func prepareMigrationTasks() []*migration {
newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate),
newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard),
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
newMigration(310, "Remove repository column num_watches", v1_23.RemoveRepoNumWatches), newMigration(310, "Remove repository column num_watches", v1_23.RemoveRepoNumWatches),
} }
return preparedMigrations return preparedMigrations

View File

@ -4,13 +4,13 @@
package v1_23 //nolint package v1_23 //nolint
import ( import (
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm" "xorm.io/xorm"
) )
func RemoveRepoNumWatches(x *xorm.Engine) error { func AddPriorityToProtectedBranch(x *xorm.Engine) error {
sess := x.NewSession() type ProtectedBranch struct {
defer sess.Close() Priority int64 `xorm:"NOT NULL DEFAULT 0"`
return base.DropTableColumns(sess, "repository", "num_watches") }
return x.Sync(new(ProtectedBranch))
} }

View File

@ -0,0 +1,16 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm"
)
func RemoveRepoNumWatches(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
return base.DropTableColumns(sess, "repository", "num_watches")
}

View File

@ -16,6 +16,31 @@ import (
"xorm.io/builder" "xorm.io/builder"
) )
type OrgList []*Organization
func (orgs OrgList) LoadTeams(ctx context.Context) (map[int64]TeamList, error) {
if len(orgs) == 0 {
return map[int64]TeamList{}, nil
}
orgIDs := make([]int64, len(orgs))
for i, org := range orgs {
orgIDs[i] = org.ID
}
teams, err := GetTeamsByOrgIDs(ctx, orgIDs)
if err != nil {
return nil, err
}
teamMap := make(map[int64]TeamList, len(orgs))
for _, team := range teams {
teamMap[team.OrgID] = append(teamMap[team.OrgID], team)
}
return teamMap, nil
}
// SearchOrganizationsOptions options to filter organizations // SearchOrganizationsOptions options to filter organizations
type SearchOrganizationsOptions struct { type SearchOrganizationsOptions struct {
db.ListOptions db.ListOptions

View File

@ -60,3 +60,14 @@ func TestGetUserOrgsList(t *testing.T) {
assert.EqualValues(t, 2, orgs[0].NumRepos) assert.EqualValues(t, 2, orgs[0].NumRepos)
} }
} }
func TestLoadOrgListTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
assert.NoError(t, err)
assert.Len(t, orgs, 1)
teamsMap, err := organization.OrgList(orgs).LoadTeams(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, teamsMap, 1)
assert.Len(t, teamsMap[3], 5)
}

View File

@ -126,3 +126,8 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T
And("team_repo.repo_id=?", repoID). And("team_repo.repo_id=?", repoID).
Find(&teams) Find(&teams)
} }
func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) {
teams := make([]*Team, 0, 10)
return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams)
}

View File

@ -0,0 +1,53 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"io"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
)
type commitChecker struct {
ctx context.Context
commitCache map[string]bool
gitRepoFacade gitrepo.Repository
gitRepo *git.Repository
gitRepoCloser io.Closer
}
func newCommitChecker(ctx context.Context, gitRepo gitrepo.Repository) *commitChecker {
return &commitChecker{ctx: ctx, commitCache: make(map[string]bool), gitRepoFacade: gitRepo}
}
func (c *commitChecker) Close() error {
if c != nil && c.gitRepoCloser != nil {
return c.gitRepoCloser.Close()
}
return nil
}
func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
exist, inCache := c.commitCache[commitID]
if inCache {
return exist
}
if c.gitRepo == nil {
r, closer, err := gitrepo.RepositoryFromContextOrOpen(c.ctx, c.gitRepoFacade)
if err != nil {
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(c.gitRepoFacade), err)
return false
}
c.gitRepo, c.gitRepoCloser = r, closer
}
exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
c.commitCache[commitID] = exist
return exist
}

View File

@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
SetUp: func() error {
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == "user2"
},
})
return nil
},
})
}

View File

@ -0,0 +1,73 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
)
type RepoComment struct {
ctx *markup.RenderContext
opts RepoCommentOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoComment) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
default:
finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
}
return finalLink
}
var _ markup.RenderHelper = (*RepoComment)(nil)
type RepoCommentOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
CurrentRefPath string // eg: "branch/main" or "commit/11223344"
}
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
helper := &RepoComment{
repoLink: repo.Link(),
opts: util.OptionalArg(opts),
}
rctx := markup.NewRenderContext(ctx)
helper.ctx = rctx
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "comment",
"markupAllowShortIssuePattern": "true",
})
}
rctx = rctx.WithHelper(helper)
return rctx
}

View File

@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoComment(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a><br/>
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a><br/>
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
// It is Gitea's old behavior, the relative path is resolved to the repo path
// It is different from GitHub, GitHub resolves relative links to current page's path
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
<a href="/user2/repo1/test" rel="nofollow">./test</a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPath", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
// the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
<a href="/user2/repo1/commit/1234/test" rel="nofollow">./test</a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
`, rendered)
})
}

View File

@ -0,0 +1,77 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
"path"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
)
type RepoFile struct {
ctx *markup.RenderContext
opts RepoFileOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoFile) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
case markup.LinkTypeRaw:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
case markup.LinkTypeMedia:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
}
return finalLink
}
var _ markup.RenderHelper = (*RepoFile)(nil)
type RepoFileOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
CurrentRefPath string // eg: "branch/main"
CurrentTreePath string // eg: "path/to/file" in the repo
}
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
helper := &RepoFile{opts: util.OptionalArg(opts)}
rctx := markup.NewRenderContext(ctx)
helper.ctx = rctx
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "document",
})
}
rctx = rctx.WithHelper(helper)
return rctx
}

View File

@ -0,0 +1,83 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoFile(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
#1
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
<a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPath", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
![/image](/image)
`)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPathByTag", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{
CurrentRefPath: "/commit/1234",
CurrentTreePath: "my-dir",
}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
<video src="/user2/repo1/media/commit/1234/my-dir/LINK">
</video>`, rendered)
})
}

View File

@ -0,0 +1,80 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
"path"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/util"
)
type RepoWiki struct {
ctx *markup.RenderContext
opts RepoWikiOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoWiki) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeMedia:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeRaw: // wiki doesn't use it
}
return finalLink
}
var _ markup.RenderHelper = (*RepoWiki)(nil)
type RepoWikiOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
// these options are not used at the moment because Wiki doesn't support sub-path, nor branch
currentRefPath string // eg: "branch/main"
currentTreePath string // eg: "path/to/file" in the repo
}
func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, opts ...RepoWikiOptions) *markup.RenderContext {
helper := &RepoWiki{opts: util.OptionalArg(opts)}
rctx := markup.NewRenderContext(ctx).WithMarkupType(markdown.MarkupName)
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeWikiMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "document",
"markupAllowShortIssuePattern": "true",
})
}
rctx = rctx.WithHelper(helper)
helper.ctx = rctx
return rctx
}

View File

@ -0,0 +1,65 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoWiki(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a>
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
<a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("PathInTag", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
<video src="/user2/repo1/wiki/raw/LINK">
</video>`, rendered)
})
}

View File

@ -0,0 +1,29 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"code.gitea.io/gitea/modules/markup"
)
type SimpleDocument struct {
*markup.SimpleRenderHelper
ctx *markup.RenderContext
baseLink string
}
func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
}
var _ markup.RenderHelper = (*SimpleDocument)(nil)
func NewRenderContextSimpleDocument(ctx context.Context, baseLink string) *markup.RenderContext {
helper := &SimpleDocument{baseLink: baseLink}
rctx := markup.NewRenderContext(ctx).WithHelper(helper).WithMetas(markup.ComposeSimpleDocumentMetas())
helper.ctx = rctx
return rctx
}

View File

@ -0,0 +1,40 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestSimpleDocument(t *testing.T) {
unittest.PrepareTestEnv(t)
rctx := NewRenderContextSimpleDocument(context.Background(), "/base").WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
<a href="/base/user2" rel="nofollow">@user2</a></p>
<p><a href="/base/test" rel="nofollow">/test</a>
<a href="/base/test" rel="nofollow">./test</a>
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="./image"/></a></p>
`, rendered)
}

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder" "xorm.io/builder"
) )
@ -64,10 +65,10 @@ func BeanExists(t assert.TestingT, bean any, conditions ...any) bool {
} }
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database // AssertExistsAndLoadBean assert that a bean exists and load it from the test database
func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...any) T { func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
exists, err := LoadBeanIfExists(bean, conditions...) exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err) require.NoError(t, err)
assert.True(t, exists, require.True(t, exists,
"Expected to find %+v (of type %T, with conditions %+v), but did not", "Expected to find %+v (of type %T, with conditions %+v), but did not",
bean, bean, conditions) bean, bean, conditions)
return bean return bean

View File

@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String()) sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close() defer sessQuery.Close()
if opts.Page != 0 { if opts.Page > 0 {
sessQuery = db.SetSessionPagination(sessQuery, opts) sessQuery = db.SetSessionPagination(sessQuery, opts)
} }

View File

@ -330,7 +330,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type=?", UserTypeIndividual). And("`user`.type=?", UserTypeIndividual).
And(isUserVisibleToViewerCond(viewer)) And(isUserVisibleToViewerCond(viewer))
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize) users := make([]*User, 0, listOptions.PageSize)
@ -352,7 +352,7 @@ func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization). And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).
And(isUserVisibleToViewerCond(viewer)) And(isUserVisibleToViewerCond(viewer))
if listOptions.Page != 0 { if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions) sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize) users := make([]*User, 0, listOptions.PageSize)

View File

@ -4,13 +4,14 @@
package webauthn package webauthn
import ( import (
"context"
"encoding/binary" "encoding/binary"
"encoding/gob" "encoding/gob"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
@ -38,40 +39,42 @@ func Init() {
} }
} }
// User represents an implementation of webauthn.User based on User model // user represents an implementation of webauthn.User based on User model
type User user_model.User type user struct {
ctx context.Context
User *user_model.User
defaultAuthFlags protocol.AuthenticatorFlags
}
var _ webauthn.User = (*user)(nil)
func NewWebAuthnUser(ctx context.Context, u *user_model.User, defaultAuthFlags ...protocol.AuthenticatorFlags) webauthn.User {
return &user{ctx: ctx, User: u, defaultAuthFlags: util.OptionalArg(defaultAuthFlags)}
}
// WebAuthnID implements the webauthn.User interface // WebAuthnID implements the webauthn.User interface
func (u *User) WebAuthnID() []byte { func (u *user) WebAuthnID() []byte {
id := make([]byte, 8) id := make([]byte, 8)
binary.PutVarint(id, u.ID) binary.PutVarint(id, u.User.ID)
return id return id
} }
// WebAuthnName implements the webauthn.User interface // WebAuthnName implements the webauthn.User interface
func (u *User) WebAuthnName() string { func (u *user) WebAuthnName() string {
if u.LoginName == "" { return util.IfZero(u.User.LoginName, u.User.Name)
return u.Name
}
return u.LoginName
} }
// WebAuthnDisplayName implements the webauthn.User interface // WebAuthnDisplayName implements the webauthn.User interface
func (u *User) WebAuthnDisplayName() string { func (u *user) WebAuthnDisplayName() string {
return (*user_model.User)(u).DisplayName() return u.User.DisplayName()
}
// WebAuthnIcon implements the webauthn.User interface
func (u *User) WebAuthnIcon() string {
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
} }
// WebAuthnCredentials implements the webauthn.User interface // WebAuthnCredentials implements the webauthn.User interface
func (u *User) WebAuthnCredentials() []webauthn.Credential { func (u *user) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID) dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
if err != nil { if err != nil {
return nil return nil
} }
return dbCreds.ToCredentials(u.defaultAuthFlags)
return dbCreds.ToCredentials()
} }

View File

@ -22,8 +22,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
_ "github.com/mattn/go-sqlite3"
) )
type codeSearchResult struct { type codeSearchResult struct {

View File

@ -133,7 +133,7 @@ func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.W
// Check if maxRows or maxSize is reached, and if true, warn. // Check if maxRows or maxSize is reached, and if true, warn.
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) { if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
warn := `<table class="data-table"><tr><td>` warn := `<table class="data-table"><tr><td>`
rawLink := ` <a href="` + ctx.RenderOptions.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RenderOptions.RelativePath) + `">` rawLink := ` <a href="` + ctx.RenderHelper.ResolveLink(util.PathEscapeSegments(ctx.RenderOptions.RelativePath), markup.LinkTypeRaw) + `">`
// Try to get the user translation // Try to get the user translation
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok { if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {

View File

@ -79,8 +79,8 @@ func envMark(envName string) string {
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
var ( var (
command = strings.NewReplacer( command = strings.NewReplacer(
envMark("GITEA_PREFIX_SRC"), ctx.RenderOptions.Links.SrcLink(), envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
envMark("GITEA_PREFIX_RAW"), ctx.RenderOptions.Links.RawLink(), envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
).Replace(p.Command) ).Replace(p.Command)
commands = strings.Fields(command) commands = strings.Fields(command)
args = commands[1:] args = commands[1:]
@ -112,14 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
args = append(args, f.Name()) args = append(args, f.Name())
} }
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderOptions.Links.SrcLink())) processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)))
defer finished() defer finished()
cmd := exec.CommandContext(processCtx, commands[0], args...) cmd := exec.CommandContext(processCtx, commands[0], args...)
cmd.Env = append( cmd.Env = append(
os.Environ(), os.Environ(),
"GITEA_PREFIX_SRC="+ctx.RenderOptions.Links.SrcLink(), "GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
"GITEA_PREFIX_RAW="+ctx.RenderOptions.Links.RawLink(), "GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
) )
if !p.IsInputFile { if !p.IsInputFile {
cmd.Stdin = input cmd.Stdin = input

View File

@ -5,9 +5,9 @@ package markup
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync" "sync"
@ -133,75 +133,49 @@ func CustomLinkURLSchemes(schemes []string) {
common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|")) common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
} }
type postProcessError struct {
context string
err error
}
func (p *postProcessError) Error() string {
return "PostProcess: " + p.context + ", " + p.err.Error()
}
type processor func(ctx *RenderContext, node *html.Node) type processor func(ctx *RenderContext, node *html.Node)
var defaultProcessors = []processor{ // PostProcessDefault does the final required transformations to the passed raw HTML
fullIssuePatternProcessor,
comparePatternProcessor,
codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
}
// PostProcess does the final required transformations to the passed raw HTML
// data, and ensures its validity. Transformations include: replacing links and // data, and ensures its validity. Transformations include: replacing links and
// emails with HTML links, parsing shortlinks in the format of [[Link]], like // emails with HTML links, parsing shortlinks in the format of [[Link]], like
// MediaWiki, linking issues in the format #ID, and mentions in the format // MediaWiki, linking issues in the format #ID, and mentions in the format
// @user, and others. // @user, and others.
func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error { func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
return postProcess(ctx, defaultProcessors, input, output) procs := []processor{
} fullIssuePatternProcessor,
comparePatternProcessor,
var commitMessageProcessors = []processor{ codePreviewPatternProcessor,
fullIssuePatternProcessor, fullHashPatternProcessor,
comparePatternProcessor, shortLinkProcessor,
fullHashPatternProcessor, linkProcessor,
linkProcessor, mentionProcessor,
mentionProcessor, issueIndexPatternProcessor,
issueIndexPatternProcessor, commitCrossReferencePatternProcessor,
commitCrossReferencePatternProcessor, hashCurrentPatternProcessor,
hashCurrentPatternProcessor, emailAddressProcessor,
emailAddressProcessor, emojiProcessor,
emojiProcessor, emojiShortCodeProcessor,
emojiShortCodeProcessor, }
return postProcess(ctx, procs, input, output)
} }
// RenderCommitMessage will use the same logic as PostProcess, but will disable // RenderCommitMessage will use the same logic as PostProcess, but will disable
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is // the shortLinkProcessor.
// set, which changes every text node into a link to the passed default link.
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) { func RenderCommitMessage(ctx *RenderContext, content string) (string, error) {
procs := commitMessageProcessors procs := []processor{
return renderProcessString(ctx, procs, content) fullIssuePatternProcessor,
} comparePatternProcessor,
fullHashPatternProcessor,
var commitMessageSubjectProcessors = []processor{ linkProcessor,
fullIssuePatternProcessor, mentionProcessor,
comparePatternProcessor, issueIndexPatternProcessor,
fullHashPatternProcessor, commitCrossReferencePatternProcessor,
linkProcessor, hashCurrentPatternProcessor,
mentionProcessor, emailAddressProcessor,
issueIndexPatternProcessor, emojiProcessor,
commitCrossReferencePatternProcessor, emojiShortCodeProcessor,
hashCurrentPatternProcessor, }
emojiShortCodeProcessor, return postProcessString(ctx, procs, content)
emojiProcessor,
} }
var emojiProcessors = []processor{ var emojiProcessors = []processor{
@ -214,7 +188,18 @@ var emojiProcessors = []processor{
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set, // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
// which changes every text node into a link to the passed default link. // which changes every text node into a link to the passed default link.
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
procs := slices.Clone(commitMessageSubjectProcessors) procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emojiShortCodeProcessor,
emojiProcessor,
}
procs = append(procs, func(ctx *RenderContext, node *html.Node) { procs = append(procs, func(ctx *RenderContext, node *html.Node) {
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data} ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
node.Type = html.ElementNode node.Type = html.ElementNode
@ -223,19 +208,19 @@ func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string)
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}}
node.FirstChild, node.LastChild = ch, ch node.FirstChild, node.LastChild = ch, ch
}) })
return renderProcessString(ctx, procs, content) return postProcessString(ctx, procs, content)
} }
// RenderIssueTitle to process title on individual issue/pull page // RenderIssueTitle to process title on individual issue/pull page
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) { func RenderIssueTitle(ctx *RenderContext, title string) (string, error) {
// do not render other issue/commit links in an issue's title - which in most cases is already a link. // do not render other issue/commit links in an issue's title - which in most cases is already a link.
return renderProcessString(ctx, []processor{ return postProcessString(ctx, []processor{
emojiShortCodeProcessor, emojiShortCodeProcessor,
emojiProcessor, emojiProcessor,
}, title) }, title)
} }
func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
var buf strings.Builder var buf strings.Builder
if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
return "", err return "", err
@ -246,7 +231,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
// RenderDescriptionHTML will use similar logic as PostProcess, but will // RenderDescriptionHTML will use similar logic as PostProcess, but will
// use a single special linkProcessor. // use a single special linkProcessor.
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) { func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
return renderProcessString(ctx, []processor{ return postProcessString(ctx, []processor{
descriptionLinkProcessor, descriptionLinkProcessor,
emojiShortCodeProcessor, emojiShortCodeProcessor,
emojiProcessor, emojiProcessor,
@ -256,11 +241,10 @@ func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
// RenderEmoji for when we want to just process emoji and shortcodes // RenderEmoji for when we want to just process emoji and shortcodes
// in various places it isn't already run through the normal markdown processor // in various places it isn't already run through the normal markdown processor
func RenderEmoji(ctx *RenderContext, content string) (string, error) { func RenderEmoji(ctx *RenderContext, content string) (string, error) {
return renderProcessString(ctx, emojiProcessors, content) return postProcessString(ctx, emojiProcessors, content)
} }
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
defer ctx.Cancel()
// FIXME: don't read all content to memory // FIXME: don't read all content to memory
rawHTML, err := io.ReadAll(input) rawHTML, err := io.ReadAll(input)
if err != nil { if err != nil {
@ -277,7 +261,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
strings.NewReader("</body></html>"), strings.NewReader("</body></html>"),
)) ))
if err != nil { if err != nil {
return &postProcessError{"invalid HTML", err} return fmt.Errorf("markup.postProcess: invalid HTML: %w", err)
} }
if node.Type == html.DocumentNode { if node.Type == html.DocumentNode {
@ -309,7 +293,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
// Render everything to buf. // Render everything to buf.
for _, node := range newNodes { for _, node := range newNodes {
if err := html.Render(output, node); err != nil { if err := html.Render(output, node); err != nil {
return &postProcessError{"error rendering processed HTML", err} return fmt.Errorf("markup.postProcess: html.Render: %w", err)
} }
} }
return nil return nil
@ -396,7 +380,7 @@ func createLink(ctx *RenderContext, href, content, class string) *html.Node {
Data: atom.A.String(), Data: atom.A.String(),
Attr: []html.Attribute{{Key: "href", Val: href}}, Attr: []html.Attribute{{Key: "href", Val: href}},
} }
if !RenderBehaviorForTesting.DisableInternalAttributes { if !RenderBehaviorForTesting.DisableAdditionalAttributes {
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"}) a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
} }
if class != "" { if class != "" {

View File

@ -51,7 +51,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L")) lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L")) lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
opts.LineStart, opts.LineStop = lineStart, lineStop opts.LineStart, opts.LineStop = lineStart, lineStop
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx, opts) h, err := DefaultRenderHelperFuncs.RenderRepoFileCodePreview(ctx, opts)
return m[0], m[1], h, err return m[0], m[1], h, err
} }

View File

@ -16,16 +16,16 @@ import (
) )
func TestRenderCodePreview(t *testing.T) { func TestRenderCodePreview(t *testing.T) {
markup.Init(&markup.ProcessorHelper{ markup.Init(&markup.RenderHelperFuncs{
RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) { RenderRepoFileCodePreview: func(ctx context.Context, options markup.RenderCodePreviewOptions) (template.HTML, error) {
return "<div>code preview</div>", nil return "<div>code preview</div>", nil
}, },
}) })
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewRenderContext(context.Background()).WithMarkupType(markdown.MarkupName), input) buffer, err := markup.RenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>") test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" data-markdown-generated-content="" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`) test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
} }

View File

@ -4,13 +4,10 @@
package markup package markup
import ( import (
"io"
"slices" "slices"
"strings" "strings"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"golang.org/x/net/html" "golang.org/x/net/html"
@ -163,15 +160,12 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that // hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
// are assumed to be in the same repository. // are assumed to be in the same repository.
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || (ctx.RenderHelper.repoFacade == nil && ctx.RenderHelper.gitRepo == nil) { if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || ctx.RenderHelper == nil {
return return
} }
start := 0 start := 0
next := node.NextSibling next := node.NextSibling
if ctx.RenderHelper.shaExistCache == nil {
ctx.RenderHelper.shaExistCache = make(map[string]bool)
}
for node != nil && node != next && start < len(node.Data) { for node != nil && node != next && start < len(node.Data) {
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
if m == nil { if m == nil {
@ -189,35 +183,12 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
// as used by git and github for linking and thus we have to do similar. // as used by git and github for linking and thus we have to do similar.
// Because of this, we check to make sure that a matched hash is actually // Because of this, we check to make sure that a matched hash is actually
// a commit in the repository before making it a link. // a commit in the repository before making it a link.
if !ctx.RenderHelper.IsCommitIDExisting(hash) {
// check cache first
exist, inCache := ctx.RenderHelper.shaExistCache[hash]
if !inCache {
if ctx.RenderHelper.gitRepo == nil {
var err error
var closer io.Closer
ctx.RenderHelper.gitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx, ctx.RenderHelper.repoFacade)
if err != nil {
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.RenderHelper.repoFacade), err)
return
}
ctx.AddCancel(func() {
_ = closer.Close()
ctx.RenderHelper.gitRepo = nil
})
}
// Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
exist = ctx.RenderHelper.gitRepo.IsReferenceExist(hash)
ctx.RenderHelper.shaExistCache[hash] = exist
}
if !exist {
start = m[3] start = m[3]
continue continue
} }
link := util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash) link := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash), LinkTypeApp)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
start = 0 start = 0
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling

View File

@ -4,7 +4,6 @@
package markup package markup
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -34,8 +33,7 @@ func numericIssueLink(baseURL, class string, index int, marker string) string {
// link an HTML link // link an HTML link
func link(href, class, contents string) string { func link(href, class, contents string) string {
extra := ` data-markdown-generated-content=""` extra := util.Iif(class != "", ` class="`+class+`"`, "")
extra += util.Iif(class != "", ` class="`+class+`"`, "")
return fmt.Sprintf(`<a href="%s"%s>%s</a>`, href, extra, contents) return fmt.Sprintf(`<a href="%s"%s>%s</a>`, href, extra, contents)
} }
@ -69,22 +67,11 @@ var localMetas = map[string]string{
"markupAllowShortIssuePattern": "true", "markupAllowShortIssuePattern": "true",
} }
var localWikiMetas = map[string]string{
"user": "test-owner",
"repo": "test-repo",
"markupContentMode": "wiki",
}
func TestRender_IssueIndexPattern(t *testing.T) { func TestRender_IssueIndexPattern(t *testing.T) {
// numeric: render inputs without valid mentions // numeric: render inputs without valid mentions
test := func(s string) { test := func(s string) {
testRenderIssueIndexPattern(t, s, s, &RenderContext{ testRenderIssueIndexPattern(t, s, s, NewTestRenderContext())
ctx: context.Background(), testRenderIssueIndexPattern(t, s, s, NewTestRenderContext(numericMetas))
})
testRenderIssueIndexPattern(t, s, s, &RenderContext{
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: numericMetas},
})
} }
// should not render anything when there are no mentions // should not render anything when there are no mentions
@ -132,10 +119,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker) links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker)
} }
expectedNil := fmt.Sprintf(expectedFmt, links...) expectedNil := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas},
})
class := "ref-issue" class := "ref-issue"
if isExternal { if isExternal {
@ -146,10 +130,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
links[i] = numericIssueLink(prefix, class, index, marker) links[i] = numericIssueLink(prefix, class, index, marker)
} }
expectedNum := fmt.Sprintf(expectedFmt, links...) expectedNum := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ testRenderIssueIndexPattern(t, s, expectedNum, NewTestRenderContext(TestAppURL, numericMetas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: numericMetas},
})
} }
// should render freestanding mentions // should render freestanding mentions
@ -183,10 +164,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
// alphanumeric: render inputs without valid mentions // alphanumeric: render inputs without valid mentions
test := func(s string) { test := func(s string) {
testRenderIssueIndexPattern(t, s, s, &RenderContext{ testRenderIssueIndexPattern(t, s, s, NewTestRenderContext(alphanumericMetas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: alphanumericMetas},
})
} }
test("") test("")
test("this is a test") test("this is a test")
@ -216,10 +194,7 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name) links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
} }
expected := fmt.Sprintf(expectedFmt, links...) expected := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expected, &RenderContext{ testRenderIssueIndexPattern(t, s, expected, NewTestRenderContext(alphanumericMetas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: alphanumericMetas},
})
} }
test("OTT-1234 test", "%s test", "OTT-1234") test("OTT-1234 test", "%s test", "OTT-1234")
test("test T-12 issue", "test %s issue", "T-12") test("test T-12 issue", "test %s issue", "T-12")
@ -239,10 +214,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
} }
expected := fmt.Sprintf(expectedFmt, links...) expected := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expected, &RenderContext{ testRenderIssueIndexPattern(t, s, expected, NewTestRenderContext(metas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
} }
test("abc ISSUE-123 def", "abc %s def", test("abc ISSUE-123 def", "abc %s def",
@ -263,10 +235,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
[]string{"ISSUE-123"}, []string{"ISSUE-123"},
) )
testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{ testRenderIssueIndexPattern(t, "will not match", "will not match", NewTestRenderContext(regexpMetas))
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: regexpMetas},
})
} }
func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) { func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
@ -278,18 +247,9 @@ func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
"style": IssueNameStyleNumeric, "style": IssueNameStyleNumeric,
} }
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{ testRenderIssueIndexPattern(t, "#1", "#1", NewTestRenderContext(metas))
ctx: context.Background(), testRenderIssueIndexPattern(t, "#1312", "#1312", NewTestRenderContext(metas))
RenderOptions: RenderOptions{Metas: metas}, testRenderIssueIndexPattern(t, "!1", "!1", NewTestRenderContext(metas))
})
testRenderIssueIndexPattern(t, "#1312", "#1312", &RenderContext{
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
testRenderIssueIndexPattern(t, "!1", "!1", &RenderContext{
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
} }
func TestRender_RenderIssueTitle(t *testing.T) { func TestRender_RenderIssueTitle(t *testing.T) {
@ -300,20 +260,12 @@ func TestRender_RenderIssueTitle(t *testing.T) {
"repo": "someRepo", "repo": "someRepo",
"style": IssueNameStyleNumeric, "style": IssueNameStyleNumeric,
} }
actual, err := RenderIssueTitle(&RenderContext{ actual, err := RenderIssueTitle(NewTestRenderContext(metas), "#1")
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
}, "#1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "#1", actual) assert.Equal(t, "#1", actual)
} }
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
ctx.RenderOptions.Links.AbsolutePrefix = true
if ctx.RenderOptions.Links.Base == "" {
ctx.RenderOptions.Links.Base = TestRepoURL
}
var buf strings.Builder var buf strings.Builder
err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf) err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf)
assert.NoError(t, err) assert.NoError(t, err)
@ -325,20 +277,12 @@ func TestRender_AutoLink(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var buffer strings.Builder var buffer strings.Builder
err := PostProcess(&RenderContext{ err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
}, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
buffer.Reset() buffer.Reset()
err = PostProcess(&RenderContext{ err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localWikiMetas, Links: Links{Base: TestRepoURL}},
}, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
} }
@ -360,14 +304,10 @@ func TestRender_AutoLink(t *testing.T) {
func TestRender_FullIssueURLs(t *testing.T) { func TestRender_FullIssueURLs(t *testing.T) {
setting.AppURL = TestAppURL setting.AppURL = TestAppURL
defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableInternalAttributes, true)() defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
var result strings.Builder var result strings.Builder
err := postProcess(&RenderContext{ err := postProcess(NewTestRenderContext(localMetas), []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result.String()) assert.Equal(t, expected, result.String())
} }

View File

@ -136,9 +136,11 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Gitea will redirect on click as appropriate. // Gitea will redirect on click as appropriate.
issuePath := util.Iif(ref.IsPull, "pulls", "issues") issuePath := util.Iif(ref.IsPull, "pulls", "issues")
if ref.Owner == "" { if ref.Owner == "" {
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue") linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp)
link = createLink(ctx, linkHref, reftext, "ref-issue")
} else { } else {
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue") linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp)
link = createLink(ctx, linkHref, reftext, "ref-issue")
} }
} }
@ -177,7 +179,8 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
} }
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
link := createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
link := createLink(ctx, linkHref, reftext, "commit")
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling

View File

@ -6,37 +6,14 @@ package markup
import ( import (
"net/url" "net/url"
"path" "path"
"path/filepath"
"strings" "strings"
"code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/markup/common"
"code.gitea.io/gitea/modules/util"
"golang.org/x/net/html" "golang.org/x/net/html"
"golang.org/x/net/html/atom" "golang.org/x/net/html/atom"
) )
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
isAnchorFragment := link != "" && link[0] == '#'
if !isAnchorFragment && !IsFullURLString(link) {
linkBase := ctx.RenderOptions.Links.Base
if ctx.IsMarkupContentWiki() {
// 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.RenderOptions.Links.WikiLink()
} else if ctx.RenderOptions.Links.BranchPath != "" || ctx.RenderOptions.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.RenderOptions.Links.SrcLink()
}
link, resolved = util.URLJoin(linkBase, link), true
}
if isAnchorFragment && userContentAnchorPrefix != "" {
link, resolved = userContentAnchorPrefix+link[1:], true
}
return link, resolved
}
func shortLinkProcessor(ctx *RenderContext, node *html.Node) { func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next {
@ -116,7 +93,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
name += tail name += tail
image := false image := false
ext := filepath.Ext(link) ext := path.Ext(link)
switch ext { switch ext {
// fast path: empty string, ignore // fast path: empty string, ignore
case "": case "":
@ -139,6 +116,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
if image { if image {
link = strings.ReplaceAll(link, " ", "+") link = strings.ReplaceAll(link, " ", "+")
} else { } else {
// the hacky wiki name encoding: space to "-"
link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-" link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
} }
if !strings.Contains(link, "/") { if !strings.Contains(link, "/") {
@ -146,9 +124,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
} }
} }
if image { if image {
if !absoluteLink { link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia)
link = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
}
title := props["title"] title := props["title"]
if title == "" { if title == "" {
title = props["alt"] title = props["alt"]
@ -174,7 +150,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
childNode.Attr = childNode.Attr[:2] childNode.Attr = childNode.Attr[:2]
} }
} else { } else {
link, _ = ResolveLink(ctx, link, "") link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault)
childNode.Type = html.TextNode childNode.Type = html.TextNode
childNode.Data = name childNode.Data = name
} }

View File

@ -33,7 +33,8 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
if ok && strings.Contains(mention, "/") { if ok && strings.Contains(mention, "/") {
mentionOrgAndTeam := strings.Split(mention, "/") mentionOrgAndTeam := strings.Split(mention, "/")
if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), "org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "" /*mention*/)) link := ctx.RenderHelper.ResolveLink(util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), LinkTypeApp)
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
start = 0 start = 0
continue continue
@ -43,8 +44,9 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
} }
mentionedUsername := mention[1:] mentionedUsername := mention[1:]
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx, mentionedUsername) { if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) {
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), mentionedUsername), mention, "" /*mention*/)) link := ctx.RenderHelper.ResolveLink(mentionedUsername, LinkTypeApp)
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
start = 0 start = 0
} else { } else {

View File

@ -4,8 +4,6 @@
package markup package markup
import ( import (
"code.gitea.io/gitea/modules/util"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@ -17,7 +15,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
} }
if IsNonEmptyRelativePath(attr.Val) { if IsNonEmptyRelativePath(attr.Val) {
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
// By default, the "<img>" tag should also be clickable, // By default, the "<img>" tag should also be clickable,
// because frontend use `<img>` to paste the re-scaled image into the markdown, // because frontend use `<img>` to paste the re-scaled image into the markdown,
@ -53,7 +51,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
continue continue
} }
if IsNonEmptyRelativePath(attr.Val) { if IsNonEmptyRelativePath(attr.Val) {
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
} }
attr.Val = camoHandleLink(attr.Val) attr.Val = camoHandleLink(attr.Val)
node.Attr[i] = attr node.Attr[i] = attr

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
"code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/gitrepo"
"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"
@ -22,44 +21,13 @@ import (
var ( var (
testRepoOwnerName = "user13" testRepoOwnerName = "user13"
testRepoName = "repo11" testRepoName = "repo11"
localMetas = map[string]string{ localMetas = map[string]string{"user": testRepoOwnerName, "repo": testRepoName}
"user": testRepoOwnerName,
"repo": testRepoName,
}
localWikiMetas = map[string]string{
"user": testRepoOwnerName,
"repo": testRepoName,
"markupContentMode": "wiki",
}
) )
type mockRepo struct {
OwnerName string
RepoName string
}
func (m *mockRepo) GetOwnerName() string {
return m.OwnerName
}
func (m *mockRepo) GetName() string {
return m.RepoName
}
func newMockRepo(ownerName, repoName string) gitrepo.Repository {
return &mockRepo{
OwnerName: ownerName,
RepoName: repoName,
}
}
func TestRender_Commits(t *testing.T) { func TestRender_Commits(t *testing.T) {
setting.AppURL = markup.TestAppURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas, newMockRepo(testRepoOwnerName, testRepoName), markup.Links{ rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
AbsolutePrefix: true, buffer, err := markup.RenderString(rctx, input)
Base: markup.TestRepoURL,
}), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
@ -102,14 +70,10 @@ func TestRender_Commits(t *testing.T) {
} }
func TestRender_CrossReferences(t *testing.T) { func TestRender_CrossReferences(t *testing.T) {
setting.AppURL = markup.TestAppURL defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas, rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
markup.Links{ buffer, err := markup.RenderString(rctx, input)
AbsolutePrefix: true,
Base: setting.AppSubURL,
}), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
@ -141,9 +105,9 @@ func TestRender_CrossReferences(t *testing.T) {
func TestRender_links(t *testing.T) { func TestRender_links(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input) buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
@ -246,9 +210,9 @@ func TestRender_links(t *testing.T) {
func TestRender_email(t *testing.T) { func TestRender_email(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
res, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input) res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
} }
@ -315,7 +279,7 @@ func TestRender_emoji(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&amp;") expected = strings.ReplaceAll(expected, "&", "&amp;")
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input) buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
@ -374,188 +338,133 @@ func TestRender_ShortLinks(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
tree := util.URLJoin(markup.TestRepoURL, "src", "master") tree := util.URLJoin(markup.TestRepoURL, "src", "master")
test := func(input, expected, expectedWiki string) { test := func(input, expected string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: markup.TestRepoURL, BranchPath: "master"}), input) buffer, err := markdown.RenderString(markup.NewTestRenderContext(tree), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: markup.TestRepoURL}, localWikiMetas), input)
assert.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") url := util.URLJoin(tree, "Link")
otherURL := util.URLJoin(tree, "Other-Link") otherURL := util.URLJoin(tree, "Other-Link")
encodedURL := util.URLJoin(tree, "Link%3F") encodedURL := util.URLJoin(tree, "Link%3F")
imgurl := util.URLJoin(mediatree, "Link.jpg") imgurl := util.URLJoin(tree, "Link.jpg")
otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg") otherImgurl := util.URLJoin(tree, "Link+Other.jpg")
encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg") encodedImgurl := util.URLJoin(tree, "Link+%23.jpg")
notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg") notencodedImgurl := util.URLJoin(tree, "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")
renderableFileURL := util.URLJoin(tree, "markdown_file.md") renderableFileURL := util.URLJoin(tree, "markdown_file.md")
renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md")
unrenderableFileURL := util.URLJoin(tree, "file.zip") unrenderableFileURL := util.URLJoin(tree, "file.zip")
unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "file.zip")
favicon := "http://google.com/favicon.ico" favicon := "http://google.com/favicon.ico"
test( test(
"[[Link]]", "[[Link]]",
`<p><a href="`+url+`" rel="nofollow">Link</a></p>`, `<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`) )
test( test(
"[[Link.-]]", "[[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/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( test(
"[[Link.jpg]]", "[[Link.jpg]]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Link.jpg" alt="Link.jpg"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Link.jpg" alt="Link.jpg"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Link.jpg" alt="Link.jpg"/></a></p>`) )
test( test(
"[["+favicon+"]]", "[["+favicon+"]]",
`<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico" alt="`+favicon+`"/></a></p>`, `<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico" alt="`+favicon+`"/></a></p>`,
`<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico" alt="`+favicon+`"/></a></p>`) )
test( test(
"[[Name|Link]]", "[[Name|Link]]",
`<p><a href="`+url+`" rel="nofollow">Name</a></p>`, `<p><a href="`+url+`" rel="nofollow">Name</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Name</a></p>`) )
test( test(
"[[Name|Link.jpg]]", "[[Name|Link.jpg]]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Name" alt="Name"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Name" alt="Name"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Name" alt="Name"/></a></p>`) )
test( test(
"[[Name|Link.jpg|alt=AltName]]", "[[Name|Link.jpg|alt=AltName]]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="AltName" alt="AltName"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="AltName" alt="AltName"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="AltName" alt="AltName"/></a></p>`) )
test( test(
"[[Name|Link.jpg|title=Title]]", "[[Name|Link.jpg|title=Title]]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="Title"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="Title"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="Title"/></a></p>`) )
test( test(
"[[Name|Link.jpg|alt=AltName|title=Title]]", "[[Name|Link.jpg|alt=AltName|title=Title]]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="AltName"/></a></p>`) )
test( test(
"[[Name|Link.jpg|alt=\"AltName\"|title='Title']]", "[[Name|Link.jpg|alt=\"AltName\"|title='Title']]",
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`, `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`,
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="AltName"/></a></p>`) )
test( test(
"[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]", "[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]",
`<p><a href="`+otherImgurl+`" rel="nofollow"><img src="`+otherImgurl+`" title="Title" alt="AltName"/></a></p>`, `<p><a href="`+otherImgurl+`" rel="nofollow"><img src="`+otherImgurl+`" title="Title" alt="AltName"/></a></p>`,
`<p><a href="`+otherImgurlWiki+`" rel="nofollow"><img src="`+otherImgurlWiki+`" title="Title" alt="AltName"/></a></p>`) )
test( test(
"[[Link]] [[Other Link]]", "[[Link]] [[Other Link]]",
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a></p>`, `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a></p>`) )
test( test(
"[[Link?]]", "[[Link?]]",
`<p><a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`, `<p><a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
`<p><a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`) )
test( test(
"[[Link]] [[Other Link]] [[Link?]]", "[[Link]] [[Other Link]] [[Link?]]",
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`, `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`) )
test( test(
"[[markdown_file.md]]", "[[markdown_file.md]]",
`<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`, `<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`,
`<p><a href="`+renderableFileURLWiki+`" rel="nofollow">markdown_file.md</a></p>`) )
test( test(
"[[file.zip]]", "[[file.zip]]",
`<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`, `<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`,
`<p><a href="`+unrenderableFileURLWiki+`" rel="nofollow">file.zip</a></p>`) )
test( test(
"[[Link #.jpg]]", "[[Link #.jpg]]",
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`, `<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`,
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`) )
test( test(
"[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]", "[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]",
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Title" alt="AltName"/></a></p>`, `<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Title" alt="AltName"/></a></p>`,
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`" title="Title" alt="AltName"/></a></p>`) )
test( test(
"[[some/path/Link #.jpg]]", "[[some/path/Link #.jpg]]",
`<p><a href="`+notencodedImgurl+`" rel="nofollow"><img src="`+notencodedImgurl+`" title="Link #.jpg" alt="some/path/Link #.jpg"/></a></p>`, `<p><a href="`+notencodedImgurl+`" rel="nofollow"><img src="`+notencodedImgurl+`" title="Link #.jpg" alt="some/path/Link #.jpg"/></a></p>`,
`<p><a href="`+notencodedImgurlWiki+`" rel="nofollow"><img src="`+notencodedImgurlWiki+`" title="Link #.jpg" alt="some/path/Link #.jpg"/></a></p>`) )
test( test(
"<p><a href=\"https://example.org\">[[foobar]]</a></p>", "<p><a href=\"https://example.org\">[[foobar]]</a></p>",
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`, `<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`,
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`) )
}
func TestRender_RelativeMedias(t *testing.T) {
render := func(input string, isWiki bool, links markup.Links) string {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(links, util.Iif(isWiki, localWikiMetas, localMetas)), input)
assert.NoError(t, err)
return strings.TrimSpace(string(buffer))
}
out := render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
assert.Equal(t, `<a href="/test-owner/test-repo/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/LINK"/></a>`, out)
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
out = render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
assert.Equal(t, `<a href="/test-owner/test-repo/media/test-branch/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/media/test-branch/LINK"/></a>`, out)
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
assert.Equal(t, `<img src="/LINK"/>`, out)
out = render(`<video src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
assert.Equal(t, `<video src="/test-owner/test-repo/LINK"></video>`, out)
out = render(`<video src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
assert.Equal(t, `<video src="/test-owner/test-repo/wiki/raw/LINK"></video>`, out)
out = render(`<video src="/LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
assert.Equal(t, `<video src="/LINK"></video>`, out)
} }
func Test_ParseClusterFuzz(t *testing.T) { func Test_ParseClusterFuzz(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
localMetas := map[string]string{ localMetas := map[string]string{"user": "go-gitea", "repo": "gitea"}
"user": "go-gitea",
"repo": "gitea",
}
data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
var res strings.Builder var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res) err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html") assert.NotContains(t, res.String(), "<html")
data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
res.Reset() res.Reset()
err = markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res) err = markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html") assert.NotContains(t, res.String(), "<html")
} }
func TestPostProcess_RenderDocument(t *testing.T) { func TestPostProcess_RenderDocument(t *testing.T) {
setting.AppURL = markup.TestAppURL
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext( err := markup.PostProcessDefault(markup.NewTestRenderContext(markup.TestAppURL, map[string]string{"user": "go-gitea", "repo": "gitea"}), strings.NewReader(input), &res)
markup.Links{
AbsolutePrefix: true,
Base: "https://example.com",
},
map[string]string{"user": "go-gitea", "repo": "gitea"},
), strings.NewReader(input), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
} }
@ -592,7 +501,7 @@ func TestIssue16020(t *testing.T) {
data := `<img src="data:image/png;base64,i//V"/>` data := `<img src="data:image/png;base64,i//V"/>`
var res strings.Builder var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, data, res.String()) assert.Equal(t, data, res.String())
} }
@ -605,23 +514,15 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var res strings.Builder var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(b, err) assert.NoError(b, err)
} }
} }
func TestFuzz(t *testing.T) { func TestFuzz(t *testing.T) {
s := "t/l/issues/8#/../../a" s := "t/l/issues/8#/../../a"
renderContext := markup.NewTestRenderContext( renderContext := markup.NewTestRenderContext()
markup.Links{ err := markup.PostProcessDefault(renderContext, strings.NewReader(s), io.Discard)
Base: "https://example.com/go-gitea/gitea",
},
map[string]string{
"user": "go-gitea",
"repo": "gitea",
},
)
err := markup.PostProcess(renderContext, strings.NewReader(s), io.Discard)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -629,7 +530,7 @@ func TestIssue18471(t *testing.T) {
data := `http://domain/org/repo/compare/783b039...da951ce` data := `http://domain/org/repo/compare/783b039...da951ce`
var res strings.Builder var res strings.Builder
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String()) assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())

View File

@ -4,11 +4,15 @@
package markup_test package markup_test
import ( import (
"os"
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
unittest.MainTest(m) setting.IsInTesting = true
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
os.Exit(m.Run())
} }

View File

@ -37,8 +37,8 @@ func NewASTTransformer(renderInternal *internal.RenderInternal) *ASTTransformer
} }
func (g *ASTTransformer) applyElementDir(n ast.Node) { func (g *ASTTransformer) applyElementDir(n ast.Node) {
if markup.DefaultProcessorHelper.ElementDir != "" { if !markup.RenderBehaviorForTesting.DisableAdditionalAttributes {
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir)) n.SetAttributeString("dir", "auto")
} }
} }
@ -80,9 +80,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting // many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
// especially in many tests. // especially in many tests.
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"] markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
if markup.RenderBehaviorForTesting.ForceHardLineBreak { if markdownLineBreakStyle == "comment" {
v.SetHardLineBreak(true)
} else if markdownLineBreakStyle == "comment" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
} else if markdownLineBreakStyle == "document" { } else if markdownLineBreakStyle == "document" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)

View File

@ -4,18 +4,15 @@
package markdown package markdown
import ( import (
"context" "os"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
markup.Init(&markup.ProcessorHelper{ setting.IsInTesting = true
IsUsernameMentionable: func(ctx context.Context, username string) bool { markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
return username == "r-lyeh" os.Exit(m.Run())
},
})
unittest.MainTest(m)
} }

View File

@ -4,11 +4,11 @@
package markdown_test package markdown_test
import ( import (
"context"
"html/template" "html/template"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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"
@ -35,60 +35,23 @@ var localMetas = map[string]string{
"repo": testRepoName, "repo": testRepoName,
} }
var localWikiMetas = map[string]string{
"user": testRepoOwnerName,
"repo": testRepoName,
"markupContentMode": "wiki",
}
type mockRepo struct {
OwnerName string
RepoName string
}
func (m *mockRepo) GetOwnerName() string {
return m.OwnerName
}
func (m *mockRepo) GetName() string {
return m.RepoName
}
func newMockRepo(ownerName, repoName string) gitrepo.Repository {
return &mockRepo{
OwnerName: ownerName,
RepoName: repoName,
}
}
func TestRender_StandardLinks(t *testing.T) { func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL test := func(input, expected string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input)
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}, localWikiMetas), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
} }
googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
test("<https://google.com/>", googleRendered, googleRendered) test("<https://google.com/>", googleRendered)
test("[Link](Link)", `<p><a href="/Link" rel="nofollow">Link</a></p>`)
lnk := util.URLJoin(FullURL, "WikiPage")
lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage")
test("[WikiPage](WikiPage)",
`<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
`<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
} }
func TestRender_Images(t *testing.T) { func TestRender_Images(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input) buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
} }
@ -122,94 +85,13 @@ func TestRender_Images(t *testing.T) {
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
} }
func testAnswers(baseURLContent, baseURLImages string) []string { func TestTotal_RenderString(t *testing.T) {
return []string{ defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
`<p>Wiki! Enjoy :)</p>
<ul>
<li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
</ul>
<p>See commit <a href="/` + testRepoOwnerName + `/` + testRepoName + `/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
<p>Ideas and codes</p>
<ul>
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="` + FullURL + `issues/786" class="ref-issue" rel="nofollow">#786</a></li>
<li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
<li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
<li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
</ul>
`,
`<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2>
<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>
<table>
<thead>
<tr>
<th><a href="` + baseURLImages + `/images/icon-install.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-install.png" title="icon-install.png" alt="images/icon-install.png"/></a></th>
<th><a href="` + baseURLContent + `/Installation" rel="nofollow">Installation</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="` + baseURLImages + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-usage.png" title="icon-usage.png" alt="images/icon-usage.png"/></a></td>
<td><a href="` + baseURLContent + `/Usage" rel="nofollow">Usage</a></td>
</tr>
</tbody>
</table>
`,
`<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
<ol>
<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a><br/>
<a href="` + baseURLImages + `/images/1.png" rel="nofollow"><img src="` + baseURLImages + `/images/1.png" title="1.png" alt="images/1.png"/></a></li>
<li>Perform a test run by hitting the Run! button.<br/>
<a href="` + baseURLImages + `/images/2.png" rel="nofollow"><img src="` + baseURLImages + `/images/2.png" title="2.png" alt="images/2.png"/></a></li>
</ol>
<h2 id="user-content-custom-id">More tests</h2>
<p>(from <a href="https://www.markdownguide.org/extended-syntax/" rel="nofollow">https://www.markdownguide.org/extended-syntax/</a>)</p>
<h3 id="user-content-checkboxes">Checkboxes</h3>
<ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="434"/>unchecked</li>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li>
</ul>
<h3 id="user-content-definition-list">Definition list</h3>
<dl>
<dt>First Term</dt>
<dd>This is the definition of the first term.</dd>
<dt>Second Term</dt>
<dd>This is one definition of the second term.</dd>
<dd>This is another definition of the second term.</dd>
</dl>
<h3 id="user-content-footnotes">Footnotes</h3>
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p>
<div>
<hr/>
<ol>
<li id="fn:user-content-1">
<p>This is the first footnote. <a href="#fnref:user-content-1" rel="nofollow"></a></p>
</li>
<li id="fn:user-content-bignote">
<p>Here is one with multiple paragraphs and code.</p>
<p>Indent paragraphs to include them in the footnote.</p>
<p><code>{ my code }</code></p>
<p>Add as many paragraphs as you like. <a href="#fnref:user-content-bignote" rel="nofollow"></a></p>
</li>
</ol>
</div>
`, `<ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="3"/> If you want to rebase/retry this PR, click this checkbox.</li>
</ul>
<hr/>
<p>This PR has been generated by <a href="https://github.com/renovatebot/renovate" rel="nofollow">Renovate Bot</a>.</p>
`,
}
}
// Test cases without ambiguous links // Test cases without ambiguous links (It is not right to copy a whole file here, instead it should clearly test what is being tested)
var sameCases = []string{ sameCases := []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]]
- [[Tips]] - [[Tips]]
@ -222,8 +104,8 @@ Ideas and codes
- Node graph editors https://github.com/ocornut/imgui/issues/306 - Node graph editors https://github.com/ocornut/imgui/issues/306
- [[Memory Editor|memory_editor_example]] - [[Memory Editor|memory_editor_example]]
- [[Plot var helper|plot_var_example]]`, - [[Plot var helper|plot_var_example]]`,
// wine-staging wiki home extract: tables, special wiki syntax, images // wine-staging wiki home extract: tables, 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).
## Quick Links ## Quick Links
@ -233,8 +115,8 @@ Here are some links to the most important topics. You can find the full list of
|--------------------------------|----------------------------------------------------------| |--------------------------------|----------------------------------------------------------|
| [[images/icon-usage.png]] | [[Usage]] | | [[images/icon-usage.png]] | [[Usage]] |
`, `,
// libgdx wiki page: inline images with special syntax // libgdx wiki page: inline images with special syntax
`[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X. `[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X.
1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop) 1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop)
[[images/1.png]] [[images/1.png]]
@ -274,7 +156,7 @@ Here is a simple footnote,[^1] and here is a longer one.[^bignote]
Add as many paragraphs as you like. Add as many paragraphs as you like.
`, `,
` `
- [ ] <!-- rebase-check --> If you want to rebase/retry this PR, click this checkbox. - [ ] <!-- rebase-check --> If you want to rebase/retry this PR, click this checkbox.
--- ---
@ -282,67 +164,101 @@ Here is a simple footnote,[^1] and here is a longer one.[^bignote]
This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!-- test-comment -->`, <!-- test-comment -->`,
}
func TestTotal_RenderWiki(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
setting.AppURL = AppURL
answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(markup.NewTestRenderContext(
markup.Links{Base: FullURL},
newMockRepo(testRepoOwnerName, testRepoName),
localWikiMetas,
), sameCases[i])
assert.NoError(t, err)
assert.Equal(t, answers[i], string(line))
} }
testCases := []string{ baseURL := ""
// Guard wiki sidebar: special syntax testAnswers := []string{
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, `<p>Wiki! Enjoy :)</p>
// rendered <ul>
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> <li><a href="` + baseURL + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + baseURL + `/Tips" rel="nofollow">Tips</a></li>
</ul>
<p>See commit <a href="/` + testRepoOwnerName + `/` + testRepoName + `/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
<p>Ideas and codes</p>
<ul>
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="` + FullURL + `issues/786" class="ref-issue" rel="nofollow">#786</a></li>
<li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
<li><a href="` + baseURL + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
<li><a href="` + baseURL + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
</ul>
`, `,
// special syntax `<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2>
`[[Name|Link]]`, <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
// rendered <h2 id="user-content-quick-links">Quick Links</h2>
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p> <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<table>
<thead>
<tr>
<th><a href="` + baseURL + `/images/icon-install.png" rel="nofollow"><img src="` + baseURL + `/images/icon-install.png" title="icon-install.png" alt="images/icon-install.png"/></a></th>
<th><a href="` + baseURL + `/Installation" rel="nofollow">Installation</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="` + baseURL + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURL + `/images/icon-usage.png" title="icon-usage.png" alt="images/icon-usage.png"/></a></td>
<td><a href="` + baseURL + `/Usage" rel="nofollow">Usage</a></td>
</tr>
</tbody>
</table>
`,
`<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
<ol>
<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a>
<a href="` + baseURL + `/images/1.png" rel="nofollow"><img src="` + baseURL + `/images/1.png" title="1.png" alt="images/1.png"/></a></li>
<li>Perform a test run by hitting the Run! button.
<a href="` + baseURL + `/images/2.png" rel="nofollow"><img src="` + baseURL + `/images/2.png" title="2.png" alt="images/2.png"/></a></li>
</ol>
<h2 id="user-content-custom-id">More tests</h2>
<p>(from <a href="https://www.markdownguide.org/extended-syntax/" rel="nofollow">https://www.markdownguide.org/extended-syntax/</a>)</p>
<h3 id="user-content-checkboxes">Checkboxes</h3>
<ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="434"/>unchecked</li>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li>
</ul>
<h3 id="user-content-definition-list">Definition list</h3>
<dl>
<dt>First Term</dt>
<dd>This is the definition of the first term.</dd>
<dt>Second Term</dt>
<dd>This is one definition of the second term.</dd>
<dd>This is another definition of the second term.</dd>
</dl>
<h3 id="user-content-footnotes">Footnotes</h3>
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p>
<div>
<hr/>
<ol>
<li id="fn:user-content-1">
<p>This is the first footnote. <a href="#fnref:user-content-1" rel="nofollow"></a></p>
</li>
<li id="fn:user-content-bignote">
<p>Here is one with multiple paragraphs and code.</p>
<p>Indent paragraphs to include them in the footnote.</p>
<p><code>{ my code }</code></p>
<p>Add as many paragraphs as you like. <a href="#fnref:user-content-bignote" rel="nofollow"></a></p>
</li>
</ol>
</div>
`,
`<ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="3"/> If you want to rebase/retry this PR, click this checkbox.</li>
</ul>
<hr/>
<p>This PR has been generated by <a href="https://github.com/renovatebot/renovate" rel="nofollow">Renovate Bot</a>.</p>
`, `,
} }
for i := 0; i < len(testCases); i += 2 { markup.Init(&markup.RenderHelperFuncs{
line, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}, localWikiMetas), testCases[i]) IsUsernameMentionable: func(ctx context.Context, username string) bool {
assert.NoError(t, err) return username == "r-lyeh"
assert.EqualValues(t, testCases[i+1], string(line)) },
} })
}
func TestTotal_RenderString(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
setting.AppURL = AppURL
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
for i := 0; i < len(sameCases); i++ { for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(markup.NewTestRenderContext( line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), sameCases[i])
markup.Links{
Base: FullURL,
BranchPath: "master",
},
newMockRepo(testRepoOwnerName, testRepoName),
localMetas,
), sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], string(line)) assert.Equal(t, testAnswers[i], string(line))
}
testCases := []string{}
for i := 0; i < len(testCases); i += 2 {
line, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), testCases[i])
assert.NoError(t, err)
assert.Equal(t, template.HTML(testCases[i+1]), line)
} }
} }
@ -395,10 +311,9 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
testcase := `![image1](/image1) testcase := `![image1](/image1)
![image2](/image2) ![image2](/image2)
` `
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br> expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a>
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p> <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
` `
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase) res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, res) assert.Equal(t, expected, res)
@ -608,372 +523,33 @@ mail@domain.com
space${SPACE}${SPACE} space${SPACE}${SPACE}
` `
input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
cases := []struct { expected := `<p>space @mention-user<br/>
Links markup.Links /just/a/path.bin
IsWiki bool <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
Expected string <a href="/file.bin" rel="nofollow">local link</a>
}{ <a href="https://example.com" rel="nofollow">remote link</a>
{ // 0 <a href="/file.bin" rel="nofollow">local link</a>
Links: markup.Links{}, <a href="https://example.com" rel="nofollow">remote link</a>
IsWiki: false, <a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
Expected: `<p>space @mention-user<br/> <a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a>
/just/a/path.bin<br/> <a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
<a href="/file.bin" rel="nofollow">local link</a><br/> <a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
<a href="/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
<a href="https://example.com" rel="nofollow">remote link</a><br/> com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/> <a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/> com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/> <span class="emoji" aria-label="thumbs up">👍</span>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/> <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/> @mention-user test
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/> #123
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p> space</p>
`, `
}, defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
{ // 1 result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
Links: markup.Links{}, assert.NoError(t, err)
IsWiki: true, assert.Equal(t, expected, string(result))
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 2
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: false,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 3
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: true,
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/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/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/>
<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/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 4
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: false,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 5
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: true,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<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/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 6
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
},
IsWiki: false,
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/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 7
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
},
IsWiki: true,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<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/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 8
Links: markup.Links{
Base: "/user/repo",
TreePath: "sub/folder",
},
IsWiki: false,
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/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/>
<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 9
Links: markup.Links{
Base: "/relative/path",
TreePath: "sub/folder",
},
IsWiki: true,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<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/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 10
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: false,
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/src/branch/main/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/branch/main/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/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 11
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: true,
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/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote 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/>
<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/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
}
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
for i, c := range cases {
result, err := markdown.RenderString(markup.NewTestRenderContext(c.Links, util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{})), input)
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
}
} }
func TestAttention(t *testing.T) { func TestAttention(t *testing.T) {

View File

@ -4,10 +4,7 @@
package markdown package markdown
import ( import (
"strings"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
giteautil "code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
) )
@ -20,10 +17,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
// Check if the destination is a real link // Check if the destination is a real link
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) { if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
v.Destination = []byte(giteautil.URLJoin( v.Destination = []byte(ctx.RenderHelper.ResolveLink(string(v.Destination), markup.LinkTypeMedia))
ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
strings.TrimLeft(string(v.Destination), "/"),
))
} }
parent := v.Parent() parent := v.Parent()

View File

@ -9,8 +9,19 @@ import (
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
) )
func resolveLink(ctx *markup.RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
isAnchorFragment := link != "" && link[0] == '#'
if !isAnchorFragment && !markup.IsFullURLString(link) {
link, resolved = ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault), true
}
if isAnchorFragment && userContentAnchorPrefix != "" {
link, resolved = userContentAnchorPrefix+link[1:], true
}
return link, resolved
}
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) { func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
if link, resolved := markup.ResolveLink(ctx, string(v.Destination), "#user-content-"); resolved { if link, resolved := resolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
v.Destination = []byte(link) v.Destination = []byte(link)
} }
} }

View File

@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/lexers"
@ -142,19 +141,11 @@ func (r *Writer) resolveLink(kind, link string) string {
// so we need to try to guess the link kind again here // so we need to try to guess the link kind again here
kind = org.RegularLink{URL: link}.Kind() kind = org.RegularLink{URL: link}.Kind()
} }
base := r.Ctx.RenderOptions.Links.Base
if r.Ctx.IsMarkupContentWiki() {
base = r.Ctx.RenderOptions.Links.WikiLink()
} else if r.Ctx.RenderOptions.Links.HasBranchInfo() {
base = r.Ctx.RenderOptions.Links.SrcLink()
}
if kind == "image" || kind == "video" { if kind == "image" || kind == "video" {
base = r.Ctx.RenderOptions.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki()) link = r.Ctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia)
} else {
link = r.Ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault)
} }
link = util.URLJoin(base, link)
} }
return link return link
} }

View File

@ -10,7 +10,6 @@ import (
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -22,34 +21,21 @@ func TestMain(m *testing.M) {
} }
func TestRender_StandardLinks(t *testing.T) { func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected string, isWiki bool) { test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext( buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/media/branch/main/"), input)
markup.Links{
Base: "/relative-path",
BranchPath: "branch/main",
},
map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
test("[[https://google.com/]]", test("[[https://google.com/]]",
`<p><a href="https://google.com/">https://google.com/</a></p>`, false) `<p><a href="https://google.com/">https://google.com/</a></p>`)
test("[[WikiPage][The WikiPage Desc]]",
`<p><a href="/relative-path/wiki/WikiPage">The WikiPage Desc</a></p>`, true)
test("[[ImageLink.svg][The Image Desc]]", test("[[ImageLink.svg][The Image Desc]]",
`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`, false) `<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
} }
func TestRender_InternalLinks(t *testing.T) { func TestRender_InternalLinks(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext( buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/src/branch/main"), input)
markup.Links{
Base: "/relative-path",
BranchPath: "branch/main",
},
), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
@ -66,7 +52,7 @@ func TestRender_InternalLinks(t *testing.T) {
func TestRender_Media(t *testing.T) { func TestRender_Media(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(markup.NewTestRenderContext(markup.Links{Base: "./relative-path"}), input) buffer, err := RenderString(markup.NewTestRenderContext("./relative-path"), input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }

View File

@ -11,8 +11,6 @@ import (
"strings" "strings"
"time" "time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/markup/internal" "code.gitea.io/gitea/modules/markup/internal"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -30,20 +28,14 @@ const (
) )
var RenderBehaviorForTesting struct { var RenderBehaviorForTesting struct {
// Markdown line break rendering has 2 default behaviors: // Gitea will emit some additional attributes for various purposes, these attributes don't affect rendering.
// * Use hard: replace "\n" with "<br>" for comments, setting.Markdown.EnableHardLineBreakInComments=true
// * Keep soft: "\n" for non-comments (a.k.a. documents), setting.Markdown.EnableHardLineBreakInDocuments=false
// In history, there was a mess:
// * The behavior was controlled by `Metas["mode"] != "document",
// * However, many places render the content without setting "mode" in Metas, all these places used comment line break setting incorrectly
ForceHardLineBreak bool
// Gitea will emit some internal attributes for various purposes, these attributes don't affect rendering.
// But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes. // But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes.
DisableInternalAttributes bool DisableAdditionalAttributes bool
} }
type RenderOptions struct { type RenderOptions struct {
UseAbsoluteLink bool
// relative path from tree root of the branch // relative path from tree root of the branch
RelativePath string RelativePath string
@ -51,12 +43,9 @@ type RenderOptions struct {
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath // for file mode, it could be left as empty, and will be detected by file extension in RelativePath
MarkupType string MarkupType string
// special link references for rendering, especially when there is a branch/tree path
Links Links
// user&repo, format&style&regexp (for external issue pattern), teams&org (for mention) // user&repo, format&style&regexp (for external issue pattern), teams&org (for mention)
// BranchNameSubURL (for iframe&asciicast) // BranchNameSubURL (for iframe&asciicast)
// markupAllowShortIssuePattern, markupContentMode (wiki) // markupAllowShortIssuePattern
// markdownLineBreakStyle (comment, document) // markdownLineBreakStyle (comment, document)
Metas map[string]string Metas map[string]string
@ -64,13 +53,6 @@ type RenderOptions struct {
InStandalonePage bool InStandalonePage bool
} }
type RenderHelper struct {
gitRepo *git.Repository
repoFacade gitrepo.Repository
shaExistCache map[string]bool
cancelFn func()
}
// RenderContext represents a render context // RenderContext represents a render context
type RenderContext struct { type RenderContext struct {
ctx context.Context ctx context.Context
@ -101,7 +83,7 @@ func (ctx *RenderContext) Value(key any) any {
var _ context.Context = (*RenderContext)(nil) var _ context.Context = (*RenderContext)(nil)
func NewRenderContext(ctx context.Context) *RenderContext { func NewRenderContext(ctx context.Context) *RenderContext {
return &RenderContext{ctx: ctx} return &RenderContext{ctx: ctx, RenderHelper: &SimpleRenderHelper{}}
} }
func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext { func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext {
@ -114,11 +96,6 @@ func (ctx *RenderContext) WithRelativePath(path string) *RenderContext {
return ctx return ctx
} }
func (ctx *RenderContext) WithLinks(links Links) *RenderContext {
ctx.RenderOptions.Links = links
return ctx
}
func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext { func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext {
ctx.RenderOptions.Metas = metas ctx.RenderOptions.Metas = metas
return ctx return ctx
@ -129,48 +106,16 @@ func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext {
return ctx return ctx
} }
func (ctx *RenderContext) WithGitRepo(r *git.Repository) *RenderContext { func (ctx *RenderContext) WithUseAbsoluteLink(v bool) *RenderContext {
ctx.RenderHelper.gitRepo = r ctx.RenderOptions.UseAbsoluteLink = v
return ctx return ctx
} }
func (ctx *RenderContext) WithRepoFacade(r gitrepo.Repository) *RenderContext { func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
ctx.RenderHelper.repoFacade = r ctx.RenderHelper = helper
return ctx return ctx
} }
// Cancel runs any cleanup functions that have been registered for this Ctx
func (ctx *RenderContext) Cancel() {
if ctx == nil {
return
}
ctx.RenderHelper.shaExistCache = map[string]bool{}
if ctx.RenderHelper.cancelFn == nil {
return
}
ctx.RenderHelper.cancelFn()
}
// AddCancel adds the provided fn as a Cleanup for this Ctx
func (ctx *RenderContext) AddCancel(fn func()) {
if ctx == nil {
return
}
oldCancelFn := ctx.RenderHelper.cancelFn
if oldCancelFn == nil {
ctx.RenderHelper.cancelFn = fn
return
}
ctx.RenderHelper.cancelFn = func() {
defer oldCancelFn()
fn()
}
}
func (ctx *RenderContext) IsMarkupContentWiki() bool {
return ctx.RenderOptions.Metas != nil && ctx.RenderOptions.Metas["markupContentMode"] == "wiki"
}
// Render renders markup file to HTML with all specific handling stuff. // Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
@ -237,6 +182,10 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
} }
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
if ctx.RenderHelper != nil {
defer ctx.RenderHelper.CleanUp()
}
finalProcessor := ctx.RenderInternal.Init(output) finalProcessor := ctx.RenderInternal.Init(output)
defer finalProcessor.Close() defer finalProcessor.Close()
@ -261,7 +210,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
eg.Go(func() (err error) { eg.Go(func() (err error) {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
err = PostProcess(ctx, pr1, pw2) err = PostProcessDefault(ctx, pr1, pw2)
} else { } else {
_, err = io.Copy(pw2, pr1) _, err = io.Copy(pw2, pr1)
} }
@ -278,11 +227,8 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
} }
// Init initializes the render global variables // Init initializes the render global variables
func Init(ph *ProcessorHelper) { func Init(renderHelpFuncs *RenderHelperFuncs) {
if ph != nil { DefaultRenderHelperFuncs = renderHelpFuncs
DefaultProcessorHelper = *ph
}
if len(setting.Markdown.CustomURLSchemes) > 0 { if len(setting.Markdown.CustomURLSchemes) > 0 {
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
} }
@ -300,23 +246,38 @@ func ComposeSimpleDocumentMetas() map[string]string {
return map[string]string{"markdownLineBreakStyle": "document"} return map[string]string{"markdownLineBreakStyle": "document"}
} }
type TestRenderHelper struct {
ctx *RenderContext
BaseLink string
}
func (r *TestRenderHelper) CleanUp() {}
func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool {
return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a")
}
func (r *TestRenderHelper) ResolveLink(link string, likeType LinkType) string {
return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
}
var _ RenderHelper = (*TestRenderHelper)(nil)
// NewTestRenderContext is a helper function to create a RenderContext for testing purpose // NewTestRenderContext is a helper function to create a RenderContext for testing purpose
// It accepts string (RelativePath), Links, map[string]string (Metas), gitrepo.Repository // It accepts string (BaseLink), map[string]string (Metas)
func NewTestRenderContext(a ...any) *RenderContext { func NewTestRenderContext(baseLinkOrMetas ...any) *RenderContext {
if !setting.IsInTesting { if !setting.IsInTesting {
panic("NewTestRenderContext should only be used in testing") panic("NewTestRenderContext should only be used in testing")
} }
ctx := NewRenderContext(context.Background()) helper := &TestRenderHelper{}
for _, v := range a { ctx := NewRenderContext(context.Background()).WithHelper(helper)
helper.ctx = ctx
for _, v := range baseLinkOrMetas {
switch v := v.(type) { switch v := v.(type) {
case string: case string:
ctx = ctx.WithRelativePath(v) helper.BaseLink = v
case Links:
ctx = ctx.WithLinks(v)
case map[string]string: case map[string]string:
ctx = ctx.WithMetas(v) ctx = ctx.WithMetas(v)
case gitrepo.Repository:
ctx = ctx.WithRepoFacade(v)
default: default:
panic(fmt.Sprintf("unknown type %T", v)) panic(fmt.Sprintf("unknown type %T", v))
} }

View File

@ -6,16 +6,52 @@ package markup
import ( import (
"context" "context"
"html/template" "html/template"
"code.gitea.io/gitea/modules/setting"
) )
// ProcessorHelper is a helper for the rendering processors (it could be renamed to RenderHelper in the future). type LinkType string
// The main purpose of this helper is to decouple some functions which are not directly available in this package.
type ProcessorHelper struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute const (
LinkTypeApp LinkType = "app" // the link is relative to the AppSubURL
LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path)
LinkTypeMedia LinkType = "media" // the link should be used to access media files (images, videos)
LinkTypeRaw LinkType = "raw" // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
)
type RenderHelper interface {
CleanUp()
// TODO: such dependency is not ideal. We should decouple the processors step by step.
// It should make the render choose different processors for different purposes,
// but not make processors to guess "is it rendering a comment or a wiki?" or "does it need to check commit ID?"
IsCommitIDExisting(commitID string) bool
ResolveLink(link string, likeType LinkType) string
}
// RenderHelperFuncs is used to decouple cycle-import
// At the moment there are different packages:
// modules/markup: basic markup rendering
// models/renderhelper: need to access models and git repo, and models/issues needs it
// services/markup: some real helper functions could only be provided here because it needs to access various services & templates
type RenderHelperFuncs struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
} }
var DefaultProcessorHelper ProcessorHelper var DefaultRenderHelperFuncs *RenderHelperFuncs
type SimpleRenderHelper struct{}
func (r *SimpleRenderHelper) CleanUp() {}
func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool {
return false
}
func (r *SimpleRenderHelper) ResolveLink(link string, likeType LinkType) string {
return resolveLinkRelative(context.Background(), setting.AppSubURL+"/", "", link, false)
}
var _ RenderHelper = (*SimpleRenderHelper)(nil)

View File

@ -0,0 +1,42 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"context"
"strings"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute bool) (finalLink string) {
if IsFullURLString(link) {
return link
}
if strings.HasPrefix(link, "/") {
if strings.HasPrefix(link, base) && strings.Count(base, "/") >= 4 {
// a trick to tolerate that some users were using absolut paths (the old gitea's behavior)
finalLink = link
} else {
finalLink = util.URLJoin(base, "./", link)
}
} else {
finalLink = util.URLJoin(base, "./", cur, link)
}
finalLink = strings.TrimSuffix(finalLink, "/")
if absolute {
finalLink = httplib.MakeAbsoluteURL(ctx, finalLink)
}
return finalLink
}
func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) (finalLink string) {
return resolveLinkRelative(ctx, base, cur, link, ctx.RenderOptions.UseAbsoluteLink)
}
func (ctx *RenderContext) ResolveLinkApp(link string) string {
return ctx.ResolveLinkRelative(setting.AppSubURL+"/", "", link)
}

View File

@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"context"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestResolveLinkRelative(t *testing.T) {
ctx := context.Background()
setting.AppURL = "http://localhost:3000"
assert.Equal(t, "/a", resolveLinkRelative(ctx, "/a", "", "", false))
assert.Equal(t, "/a/b", resolveLinkRelative(ctx, "/a", "b", "", false))
assert.Equal(t, "/a/b/c", resolveLinkRelative(ctx, "/a", "b", "c", false))
assert.Equal(t, "/a/c", resolveLinkRelative(ctx, "/a", "b", "/c", false))
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))
// some users might have used absolute paths a lot, so if the prefix overlaps and has enough slashes, we should tolerate it
assert.Equal(t, "/owner/repo/foo/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo", "", "/owner/repo/foo/bar/xxx", false))
assert.Equal(t, "/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo/bar", "", "/owner/repo/foo/bar/xxx", false))
}

View File

@ -1,56 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
type Links struct {
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), usually it is the path to the repo
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 {
if l.AbsolutePrefix {
return setting.AppURL
}
return setting.AppSubURL
}
func (l *Links) HasBranchInfo() bool {
return l.BranchPath != ""
}
func (l *Links) SrcLink() string {
return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
}
func (l *Links) MediaLink() string {
return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
}
func (l *Links) RawLink() string {
return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
}
func (l *Links) WikiLink() string {
return util.URLJoin(l.Base, "wiki")
}
func (l *Links) WikiRawLink() string {
return util.URLJoin(l.Base, "wiki/raw")
}
func (l *Links) ResolveMediaLink(isWiki bool) string {
if isWiki {
return l.WikiRawLink()
} else if l.HasBranchInfo() {
return l.MediaLink()
}
return l.Base
}

View File

@ -6,7 +6,7 @@ package markup
import ( import (
"bytes" "bytes"
"io" "io"
"path/filepath" "path"
"strings" "strings"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -55,7 +55,7 @@ func RegisterRenderer(renderer Renderer) {
// GetRendererByFileName get renderer by filename // GetRendererByFileName get renderer by filename
func GetRendererByFileName(filename string) Renderer { func GetRendererByFileName(filename string) Renderer {
extension := strings.ToLower(filepath.Ext(filename)) extension := strings.ToLower(path.Ext(filename))
return extRenderers[extension] return extRenderers[extension]
} }

View File

@ -26,6 +26,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
// Chroma always uses 1-2 letters for style names, we could tolerate it at the moment
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^\w{0,2}$`)).OnElements("span")
// Custom URL-Schemes // Custom URL-Schemes
if len(setting.Markdown.CustomURLSchemes) > 0 { if len(setting.Markdown.CustomURLSchemes) > 0 {
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)

View File

@ -19,6 +19,7 @@ func TestSanitizer(t *testing.T) {
// Code highlighting class // Code highlighting class
`<code class="random string"></code>`, `<code></code>`, `<code class="random string"></code>`, `<code></code>`,
`<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`, `<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`,
`<span class="k"></span><span class="nb"></span>`, `<span class="k"></span><span class="nb"></span>`,
// Input checkbox // Input checkbox
`<input type="hidden">`, ``, `<input type="hidden">`, ``,

View File

@ -25,6 +25,7 @@ type BranchProtection struct {
// Deprecated: true // Deprecated: true
BranchName string `json:"branch_name"` BranchName string `json:"branch_name"`
RuleName string `json:"rule_name"` RuleName string `json:"rule_name"`
Priority int64 `json:"priority"`
EnablePush bool `json:"enable_push"` EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"` EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
@ -64,6 +65,7 @@ type CreateBranchProtectionOption struct {
// Deprecated: true // Deprecated: true
BranchName string `json:"branch_name"` BranchName string `json:"branch_name"`
RuleName string `json:"rule_name"` RuleName string `json:"rule_name"`
Priority int64 `json:"priority"`
EnablePush bool `json:"enable_push"` EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"` EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
@ -96,6 +98,7 @@ type CreateBranchProtectionOption struct {
// EditBranchProtectionOption options for editing a branch protection // EditBranchProtectionOption options for editing a branch protection
type EditBranchProtectionOption struct { type EditBranchProtectionOption struct {
Priority *int64 `json:"priority"`
EnablePush *bool `json:"enable_push"` EnablePush *bool `json:"enable_push"`
EnablePushWhitelist *bool `json:"enable_push_whitelist"` EnablePushWhitelist *bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
@ -125,3 +128,8 @@ type EditBranchProtectionOption struct {
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"` UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
BlockAdminMergeOverride *bool `json:"block_admin_merge_override"` BlockAdminMergeOverride *bool `json:"block_admin_merge_override"`
} }
// UpdateBranchProtectionPriories a list to update the branch protection rule priorities
type UpdateBranchProtectionPriories struct {
IDs []int64 `json:"ids"`
}

View File

@ -59,7 +59,7 @@ func TestMain(m *testing.M) {
if err := git.InitSimple(context.Background()); err != nil { if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err) log.Fatal("git init failed, err: %v", err)
} }
markup.Init(&markup.ProcessorHelper{ markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx context.Context, username string) bool { IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == "mention-user" return username == "mention-user"
}, },
@ -74,7 +74,7 @@ func newTestRenderUtils() *RenderUtils {
} }
func TestRenderCommitBody(t *testing.T) { func TestRenderCommitBody(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
type args struct { type args struct {
msg string msg string
} }
@ -145,7 +145,7 @@ func TestRenderCommitMessageLinkSubject(t *testing.T) {
} }
func TestRenderIssueTitle(t *testing.T) { func TestRenderIssueTitle(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
expected := ` space @mention-user<SPACE><SPACE> expected := ` space @mention-user<SPACE><SPACE>
/just/a/path.bin /just/a/path.bin
https://example.com/file.bin https://example.com/file.bin
@ -172,7 +172,7 @@ mail@domain.com
} }
func TestRenderMarkdownToHtml(t *testing.T) { func TestRenderMarkdownToHtml(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/> expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
/just/a/path.bin /just/a/path.bin
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
@ -211,6 +211,7 @@ func TestRenderLabels(t *testing.T) {
} }
func TestUserMention(t *testing.T) { func TestUserMention(t *testing.T) {
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user") rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user")
assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" data-markdown-generated-content="" rel="nofollow">@mention-user</a> <a href="/mention-user" data-markdown-generated-content="" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered))) assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" rel="nofollow">@mention-user</a> <a href="/mention-user" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered)))
} }

140
package-lock.json generated
View File

@ -110,7 +110,8 @@
"type-fest": "4.26.1", "type-fest": "4.26.1",
"updates": "16.4.0", "updates": "16.4.0",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.4" "vitest": "2.1.4",
"vue-tsc": "2.1.10"
}, },
"engines": { "engines": {
"node": ">= 18.0.0" "node": ">= 18.0.0"
@ -5390,6 +5391,35 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@volar/language-core": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz",
"integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.10"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz",
"integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz",
"integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.10",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.12", "version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
@ -5449,6 +5479,58 @@
"@vue/shared": "3.5.12" "@vue/shared": "3.5.12"
} }
}, },
"node_modules/@vue/compiler-vue2": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true,
"license": "MIT",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/@vue/language-core": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz",
"integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "~2.4.8",
"@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.0",
"alien-signals": "^0.2.0",
"minimatch": "^9.0.3",
"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@vue/language-core/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.12", "version": "3.5.12",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz",
@ -5821,6 +5903,13 @@
"ajv": "^8.8.2" "ajv": "^8.8.2"
} }
}, },
"node_modules/alien-signals": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz",
"integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==",
"dev": true,
"license": "MIT"
},
"node_modules/ansi_up": { "node_modules/ansi_up": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.2.tgz", "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.2.tgz",
@ -7484,6 +7573,13 @@
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -10337,6 +10433,16 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/hosted-git-info": { "node_modules/hosted-git-info": {
"version": "2.8.9", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -11793,6 +11899,13 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": { "node_modules/mz": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@ -12168,6 +12281,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true,
"license": "MIT"
},
"node_modules/path-data-parser": { "node_modules/path-data-parser": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
@ -15521,6 +15641,24 @@
} }
} }
}, },
"node_modules/vue-tsc": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz",
"integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "~2.4.8",
"@vue/language-core": "2.1.10",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",

View File

@ -109,7 +109,8 @@
"type-fest": "4.26.1", "type-fest": "4.26.1",
"updates": "16.4.0", "updates": "16.4.0",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.4" "vitest": "2.1.4",
"vue-tsc": "2.1.10"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

View File

@ -321,7 +321,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
} }
if !allow { if !allow {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope))
return return
} }
@ -1204,6 +1204,7 @@ func Routes() *web.Router {
m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection) m.Delete("", repo.DeleteBranchProtection)
}) })
m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories)
}, reqToken(), reqAdmin()) }, reqToken(), reqAdmin())
m.Group("/tags", func() { m.Group("/tags", func() {
m.Get("", repo.ListTags) m.Get("", repo.ListTags)
@ -1377,6 +1378,8 @@ func Routes() *web.Router {
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar) m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
m.Delete("", repo.DeleteAvatar) m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken()) }, reqAdmin(), reqToken())
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
}, repoAssignment(), checkTokenPublicOnly()) }, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

View File

@ -7,15 +7,19 @@ import (
go_context "context" go_context "context"
"io" "io"
"net/http" "net/http"
"os"
"path" "path"
"strings" "strings"
"testing" "testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -23,10 +27,17 @@ import (
const AppURL = "http://localhost:3000/" const AppURL = "http://localhost:3000/"
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
})
os.Exit(m.Run())
}
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) { func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
setting.AppURL = AppURL setting.AppURL = AppURL
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
context := "/gogits/gogs" context := "/user2/repo1"
if !wiki { if !wiki {
context += path.Join("/src/branch/main", path.Dir(filePath)) context += path.Join("/src/branch/main", path.Dir(filePath))
} }
@ -38,6 +49,8 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
FilePath: filePath, FilePath: filePath,
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
ctx.Repo = &context_service.Repository{}
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
web.SetForm(ctx, &options) web.SetForm(ctx, &options)
Markup(ctx) Markup(ctx)
assert.Equal(t, expectedBody, resp.Body.String()) assert.Equal(t, expectedBody, resp.Body.String())
@ -46,9 +59,9 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
} }
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) { func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
setting.AppURL = AppURL setting.AppURL = AppURL
context := "/gogits/gogs" context := "/user2/repo1"
if !wiki { if !wiki {
context += "/src/branch/main" context += "/src/branch/main"
} }
@ -67,7 +80,8 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
} }
func TestAPI_RenderGFM(t *testing.T) { func TestAPI_RenderGFM(t *testing.T) {
markup.Init(&markup.ProcessorHelper{ unittest.PrepareTestEnv(t)
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx go_context.Context, username string) bool { IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
return username == "r-lyeh" return username == "r-lyeh"
}, },
@ -82,20 +96,20 @@ func TestAPI_RenderGFM(t *testing.T) {
// rendered // rendered
`<p>Wiki! Enjoy :)</p> `<p>Wiki! Enjoy :)</p>
<ul> <ul>
<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/user2/repo1/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><a href="http://localhost:3000/user2/repo1/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> <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="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> `<p><a href="http://localhost:3000/user2/repo1/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="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p> `<p><a href="http://localhost:3000/user2/repo1/wiki/Link" rel="nofollow">Name</a></p>
`, `,
// empty // empty
``, ``,
@ -119,8 +133,8 @@ 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="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a> <p><a href="http://localhost:3000/user2/repo1/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> <a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`, `,
} }
@ -143,20 +157,20 @@ Here are some links to the most important topics. You can find the full list of
} }
input := "[Link](test.md)\n![Image](image.png)" 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> testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/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> <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK) `, http.StatusOK)
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a> testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/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> <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK) `, http.StatusOK)
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a> testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/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> <a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK) `, 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> testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/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> <a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
`, http.StatusOK) `, http.StatusOK)
testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)
@ -182,10 +196,11 @@ var simpleCases = []string{
func TestAPI_RenderSimple(t *testing.T) { func TestAPI_RenderSimple(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: "markdown", Mode: "markdown",
Text: "", Text: "",
Context: "/gogits/gogs", Context: "/user2/repo1",
} }
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 {
@ -199,6 +214,7 @@ func TestAPI_RenderSimple(t *testing.T) {
func TestAPI_RenderRaw(t *testing.T) { func TestAPI_RenderRaw(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
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 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i])) ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))

View File

@ -618,6 +618,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
protectBranch = &git_model.ProtectedBranch{ protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
RuleName: ruleName, RuleName: ruleName,
Priority: form.Priority,
CanPush: form.EnablePush, CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
@ -640,7 +641,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
BlockAdminMergeOverride: form.BlockAdminMergeOverride, BlockAdminMergeOverride: form.BlockAdminMergeOverride,
} }
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ if err := git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers, UserIDs: whitelistUsers,
TeamIDs: whitelistTeams, TeamIDs: whitelistTeams,
ForcePushUserIDs: forcePushAllowlistUsers, ForcePushUserIDs: forcePushAllowlistUsers,
@ -649,14 +650,13 @@ func CreateBranchProtection(ctx *context.APIContext) {
MergeTeamIDs: mergeWhitelistTeams, MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers, ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams, ApprovalsTeamIDs: approvalsWhitelistTeams,
}) }); err != nil {
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err)
return return
} }
if isBranchExist { if isBranchExist {
if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil { if err := pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil {
ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err) ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
return return
} }
@ -796,6 +796,10 @@ func EditBranchProtection(ctx *context.APIContext) {
} }
} }
if form.Priority != nil {
protectBranch.Priority = *form.Priority
}
if form.EnableMergeWhitelist != nil { if form.EnableMergeWhitelist != nil {
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
} }
@ -1080,3 +1084,47 @@ func DeleteBranchProtection(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
// UpdateBranchProtectionPriories updates the priorities of branch protections for a repo
func UpdateBranchProtectionPriories(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/branch_protections/priority repository repoUpdateBranchProtectionPriories
// ---
// summary: Update the priorities of branch protections for a repository.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateBranchProtectionPriories"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.UpdateBranchProtectionPriories)
repo := ctx.Repo.Repository
if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProtectBranchPriorities", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -0,0 +1,53 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
)
func DownloadArchive(ctx *context.APIContext) {
var tp git.ArchiveType
switch ballType := ctx.PathParam("ball_type"); ballType {
case "tarball":
tp = git.TARGZ
case "zipball":
tp = git.ZIP
case "bundle":
tp = git.BUNDLE
default:
ctx.Error(http.StatusBadRequest, "", fmt.Sprintf("Unknown archive type: %s", ballType))
return
}
if ctx.Repo.GitRepo == nil {
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
}
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
}
r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp)
if err != nil {
ctx.ServerError("NewRequest", err)
return
}
archive, err := r.Await(ctx)
if err != nil {
ctx.ServerError("archive.Await", err)
return
}
download(ctx, r.GetArchiveName(), archive)
}

View File

@ -301,7 +301,13 @@ func GetArchive(ctx *context.APIContext) {
func archiveDownload(ctx *context.APIContext) { func archiveDownload(ctx *context.APIContext) {
uri := ctx.PathParam("*") uri := ctx.PathParam("*")
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) ext, tp, err := archiver_service.ParseFileName(uri)
if err != nil {
ctx.Error(http.StatusBadRequest, "ParseFileName", err)
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
if err != nil { if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.Error(http.StatusBadRequest, "unknown archive format", err) ctx.Error(http.StatusBadRequest, "unknown archive format", err)
@ -327,9 +333,12 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model.
// Add nix format link header so tarballs lock correctly: // Add nix format link header so tarballs lock correctly:
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md // https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`, ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`,
ctx.Repo.Repository.APIURL(), ctx.Repo.Repository.APIURL(),
archiver.CommitID, archiver.CommitID)) archiver.CommitID,
archiver.Type.String(),
archiver.CommitID,
))
rPath := archiver.RelativePath() rPath := archiver.RelativePath()
if setting.RepoArchive.Storage.ServeDirect() { if setting.RepoArchive.Storage.ServeDirect() {

View File

@ -146,6 +146,9 @@ type swaggerParameterBodies struct {
// in:body // in:body
EditBranchProtectionOption api.EditBranchProtectionOption EditBranchProtectionOption api.EditBranchProtectionOption
// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
// in:body // in:body
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions

View File

@ -11,6 +11,8 @@ import (
"path" "path"
"strings" "strings"
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/httplib" "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"
@ -20,7 +22,7 @@ import (
) )
// 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, urlPathContext, filePath string) { func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, urlPathContext, filePath string) {
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}" // 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 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 // filePath will be used as RenderContext.RelativePath
@ -28,60 +30,69 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md" // 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" // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
renderCtx := markup.NewRenderContext(ctx).
WithLinks(markup.Links{AbsolutePrefix: true}).
WithMarkupType(markdown.MarkupName)
if urlPathContext != "" {
renderCtx.RenderOptions.Links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
}
if mode == "" || mode == "markdown" { if mode == "" || mode == "markdown" {
// raw markdown doesn't need any special handling // raw markdown doesn't need any special handling
if err := markdown.RenderRaw(renderCtx, strings.NewReader(text), ctx.Resp); err != nil { baseLink := urlPathContext
if baseLink == "" {
baseLink = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
}
rctx := renderhelper.NewRenderContextSimpleDocument(ctx, baseLink).WithUseAbsoluteLink(true).
WithMarkupType(markdown.MarkupName)
if err := markdown.RenderRaw(rctx, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
} }
return return
} }
// Ideally, this handler should be called with RepoAssigment and get the related repo from context "/owner/repo/markup"
// then render could use the repo to do various things (the permission check has passed)
//
// However, this handler is also exposed as "/markup" without any repo context,
// then since there is no permission check, so we can't use the repo from "context" parameter,
// in this case, only the "path" information could be used which doesn't cause security problems.
var repoModel *repo.Repository
if ctxRepo != nil {
repoModel = ctxRepo.Repository
}
var repoOwnerName, repoName, refPath, treePath string
repoLinkPath := strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/")
fields := strings.SplitN(repoLinkPath, "/", 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}"
repoOwnerName, repoName = fields[0], fields[1]
treePath = 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, "/"+treePath) // now we get the correct branch path: "branch/features/feat-12"
} else if fields = strings.SplitN(repoLinkPath, "/", 3); len(fields) == 2 {
repoOwnerName, repoName = fields[0], fields[1]
}
var rctx *markup.RenderContext
switch mode { switch mode {
case "gfm": // legacy mode, do nothing case "gfm": // legacy mode
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
CurrentRefPath: refPath, CurrentTreePath: treePath,
})
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "comment": case "comment":
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "comment"}) rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "wiki": case "wiki":
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"}) rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "file": case "file":
// render the repo file content by its extension rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "document"}). DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
WithMarkupType(""). CurrentRefPath: refPath, CurrentTreePath: treePath,
WithRelativePath(filePath) })
rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension
default: default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
return return
} }
rctx = rctx.WithUseAbsoluteLink(true)
fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5) if err := markup.Render(rctx, strings.NewReader(text), ctx.Resp); err != nil {
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"
renderCtx = renderCtx.WithLinks(markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir})
}
if repo != nil && repo.Repository != nil {
renderCtx = renderCtx.WithRepoFacade(repo.Repository)
if mode == "file" {
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeDocumentMetas(ctx))
} else if mode == "wiki" {
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeWikiMetas(ctx))
} else if mode == "comment" {
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeMetas(ctx))
}
}
if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusUnprocessableEntity, err.Error()) ctx.Error(http.StatusUnprocessableEntity, err.Error())
} else { } else {

View File

@ -154,7 +154,7 @@ func ActivateEmail(ctx *context.Context) {
// DeleteEmail serves a POST request for delete a user's email // DeleteEmail serves a POST request for delete a user's email
func DeleteEmail(ctx *context.Context) { func DeleteEmail(ctx *context.Context) {
u, err := user_model.GetUserByID(ctx, ctx.FormInt64("Uid")) u, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
if err != nil || u == nil { if err != nil || u == nil {
ctx.ServerError("GetUserByID", err) ctx.ServerError("GetUserByID", err)
return return

View File

@ -76,8 +76,17 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
}() }()
// Validate the parsed response. // Validate the parsed response.
// ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
var user *user_model.User var user *user_model.User
cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) { parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
ctx.Status(http.StatusForbidden)
return
}
cred, err := wa.WebAuthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
userID, n := binary.Varint(userHandle) userID, n := binary.Varint(userHandle)
if n <= 0 { if n <= 0 {
return nil, errors.New("invalid rawID") return nil, errors.New("invalid rawID")
@ -89,8 +98,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
return nil, err return nil, err
} }
return (*wa.User)(user), nil return wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags), nil
}, *sessionData, ctx.Req) }, *sessionData, parsedResponse)
if err != nil { if err != nil {
// Failed authentication attempt. // Failed authentication attempt.
log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err) log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err)
@ -171,7 +180,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
return return
} }
assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user)) webAuthnUser := wa.NewWebAuthnUser(ctx, user)
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
if err != nil { if err != nil {
ctx.ServerError("webauthn.BeginLogin", err) ctx.ServerError("webauthn.BeginLogin", err)
return return
@ -216,7 +226,8 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
} }
// Validate the parsed response. // Validate the parsed response.
cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse) webAuthnUser := wa.NewWebAuthnUser(ctx, user, parsedResponse.Response.AuthenticatorData.Flags)
cred, err := wa.WebAuthn.ValidateLogin(webAuthnUser, *sessionData, parsedResponse)
if err != nil { if err != nil {
// Failed authentication attempt. // Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err) log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)

View File

@ -24,12 +24,12 @@ func List(ctx *context.Context) {
var subNames []string var subNames []string
for _, tmplName := range templateNames { for _, tmplName := range templateNames {
subName := strings.TrimSuffix(tmplName, ".tmpl") subName := strings.TrimSuffix(tmplName, ".tmpl")
if subName != "list" { if !strings.HasPrefix(subName, "devtest-") {
subNames = append(subNames, subName) subNames = append(subNames, subName)
} }
} }
ctx.Data["SubNames"] = subNames ctx.Data["SubNames"] = subNames
ctx.HTML(http.StatusOK, "devtest/list") ctx.HTML(http.StatusOK, "devtest/devtest-list")
} }
func FetchActionTest(ctx *context.Context) { func FetchActionTest(ctx *context.Context) {

View File

@ -13,8 +13,8 @@ import (
"strings" "strings"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"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/templates" "code.gitea.io/gitea/modules/templates"
@ -48,22 +48,18 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
return act.GetRepoAbsoluteLink(ctx) + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch()) return act.GetRepoAbsoluteLink(ctx) + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch())
} }
// renderMarkdown creates a minimal markdown render context from an action. // renderCommentMarkdown renders the comment markdown to html
// If rendering fails, the original markdown text is returned func renderCommentMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML { act.LoadRepo(ctx)
markdownCtx := markup.NewRenderContext(ctx). if act.Repo == nil {
WithLinks(markup.Links{ return ""
Base: act.GetRepoLink(ctx),
}).
WithMetas(map[string]string{ // FIXME: not right here, it should use issue to compose the metas
"user": act.GetRepoUserName(ctx),
"repo": act.GetRepoName(ctx),
})
markdown, err := markdown.RenderString(markdownCtx, content)
if err != nil {
return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl
} }
return markdown rctx := renderhelper.NewRenderContextRepoComment(ctx, act.Repo).WithUseAbsoluteLink(true)
rendered, err := markdown.RenderString(rctx, content)
if err != nil {
return ""
}
return rendered
} }
// feedActionsToFeedItems convert gitea's Action feed to feeds Item // feedActionsToFeedItems convert gitea's Action feed to feeds Item
@ -225,12 +221,12 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
case activities_model.ActionCreateIssue, activities_model.ActionCreatePullRequest: case activities_model.ActionCreateIssue, activities_model.ActionCreatePullRequest:
desc = strings.Join(act.GetIssueInfos(), "#") desc = strings.Join(act.GetIssueInfos(), "#")
content = renderMarkdown(ctx, act, act.GetIssueContent(ctx)) content = renderCommentMarkdown(ctx, act, act.GetIssueContent(ctx))
case activities_model.ActionCommentIssue, activities_model.ActionApprovePullRequest, activities_model.ActionRejectPullRequest, activities_model.ActionCommentPull: case activities_model.ActionCommentIssue, activities_model.ActionApprovePullRequest, activities_model.ActionRejectPullRequest, activities_model.ActionCommentPull:
desc = act.GetIssueTitle(ctx) desc = act.GetIssueTitle(ctx)
comment := act.GetIssueInfos()[1] comment := act.GetIssueInfos()[1]
if len(comment) != 0 { if len(comment) != 0 {
desc += "\n\n" + string(renderMarkdown(ctx, act, comment)) desc += "\n\n" + string(renderCommentMarkdown(ctx, act, comment))
} }
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
desc = act.GetIssueInfos()[1] desc = act.GetIssueInfos()[1]
@ -294,12 +290,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (
} }
link := &feeds.Link{Href: rel.HTMLURL()} link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, rel.Repo).WithUseAbsoluteLink(true)
WithRepoFacade(rel.Repo). content, err = markdown.RenderString(rctx,
WithLinks(markup.Links{
Base: rel.Repo.Link(),
}).
WithMetas(rel.Repo.ComposeMetas(ctx)),
rel.Note) rel.Note)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -7,7 +7,7 @@ import (
"time" "time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -41,9 +41,8 @@ func showUserFeed(ctx *context.Context, formatType string) {
return return
} }
ctxUserDescription, err := markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextSimpleDocument(ctx, ctx.ContextUser.HTMLURL())
WithLinks(markup.Links{Base: ctx.ContextUser.HTMLURL()}). ctxUserDescription, err := markdown.RenderString(rctx,
WithMetas(markup.ComposeSimpleDocumentMetas()),
ctx.ContextUser.Description) ctx.ContextUser.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View File

@ -11,10 +11,10 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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/modules/util"
@ -180,16 +180,10 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err) log.Error("failed to GetBlobContent: %v", err)
} else { } else {
if profileContent, err := markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
WithGitRepo(profileGitRepo). CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
WithLinks(markup.Links{ })
// Pass repo link to markdown render for the full link of media elements. if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
// The profile of default branch would be shown.
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
}).
WithMetas(markup.ComposeSimpleDocumentMetas()),
bytes); err != nil {
log.Error("failed to RenderString: %v", err) log.Error("failed to RenderString: %v", err)
} else { } else {
ctx.Data["ProfileReadme"] = profileContent ctx.Data["ProfileReadme"] = profileContent

View File

@ -15,6 +15,7 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -392,15 +393,8 @@ func Diff(ctx *context.Context) {
if err == nil { if err == nil {
ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))})
WithLinks(markup.Links{ ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
Base: ctx.Repo.RepoLink,
BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
if err != nil { if err != nil {
ctx.ServerError("RenderCommitMessage", err) ctx.ServerError("RenderCommitMessage", err)
return return

View File

@ -18,12 +18,12 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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/optional" "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -366,12 +366,8 @@ func UpdateIssueContent(ctx *context.Context) {
} }
} }
content, err := markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.FormString("context")}). content, err := markdown.RenderString(rctx, issue.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -10,10 +10,10 @@ import (
"net/http" "net/http"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -267,12 +267,8 @@ func UpdateCommentContent(ctx *context.Context) {
var renderedContent template.HTML var renderedContent template.HTML
if comment.Content != "" { if comment.Content != "" {
renderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.FormString("context")}). renderedContent, err = markdown.RenderString(rctx, comment.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -19,6 +19,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -359,12 +360,8 @@ func ViewIssue(ctx *context.Context) {
} }
} }
ctx.Data["IssueWatch"] = iw ctx.Data["IssueWatch"] = iw
issue.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return
@ -464,14 +461,8 @@ func ViewIssue(ctx *context.Context) {
comment.Issue = issue comment.Issue = issue
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
comment.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx = renderhelper.NewRenderContextRepoComment(ctx, repo)
WithLinks(markup.Links{ comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
Base: ctx.Repo.RepoLink,
}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return
@ -546,12 +537,8 @@ func ViewIssue(ctx *context.Context) {
} }
} }
} else if comment.Type.HasContentSupport() { } else if comment.Type.HasContentSupport() {
comment.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx = renderhelper.NewRenderContextRepoComment(ctx, repo)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -10,8 +10,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -79,12 +79,8 @@ func Milestones(ctx *context.Context) {
} }
} }
for _, m := range miles { for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). m.RenderedContent, err = markdown.RenderString(rctx, m.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
m.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return
@ -265,12 +261,8 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
return return
} }
milestone.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). milestone.RenderedContent, err = markdown.RenderString(rctx, milestone.Content)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
milestone.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -13,11 +13,11 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -92,12 +92,8 @@ func Projects(ctx *context.Context) {
} }
for i := range projects { for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
projects[i].Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return
@ -422,12 +418,8 @@ func ViewProject(ctx *context.Context) {
ctx.Data["SelectLabels"] = selectLabels ctx.Data["SelectLabels"] = selectLabels
ctx.Data["AssigneeID"] = assigneeID ctx.Data["AssigneeID"] = assigneeID
project.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). project.RenderedContent, err = markdown.RenderString(rctx, project.Description)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
project.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -13,13 +13,13 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -114,12 +114,8 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
cacheUsers[r.PublisherID] = r.Publisher cacheUsers[r.PublisherID] = r.Publisher
} }
r.RenderedNote, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo)
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}). r.RenderedNote, err = markdown.RenderString(rctx, r.Note)
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
r.Note)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"path" "path"
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -56,17 +57,12 @@ func RenderFile(ctx *context.Context) {
return return
} }
err = markup.Render(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
WithRelativePath(ctx.Repo.TreePath). CurrentRefPath: ctx.Repo.BranchNameSubURL(),
WithLinks(markup.Links{ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
Base: ctx.Repo.RepoLink, }).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true)
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath), err = markup.Render(rctx, rd, ctx.Resp)
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithInStandalonePage(true),
rd, ctx.Resp)
if err != nil { if err != nil {
log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err) log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError) http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)

View File

@ -469,7 +469,12 @@ func RedirectDownload(ctx *context.Context) {
// Download an archive of a repository // Download an archive of a repository
func Download(ctx *context.Context) { func Download(ctx *context.Context) {
uri := ctx.PathParam("*") uri := ctx.PathParam("*")
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) ext, tp, err := archiver_service.ParseFileName(uri)
if err != nil {
ctx.ServerError("ParseFileName", err)
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
if err != nil { if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.Error(http.StatusBadRequest, err.Error()) ctx.Error(http.StatusBadRequest, err.Error())
@ -528,7 +533,12 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
// kind of drop it on the floor if this is the case. // kind of drop it on the floor if this is the case.
func InitiateDownload(ctx *context.Context) { func InitiateDownload(ctx *context.Context) {
uri := ctx.PathParam("*") uri := ctx.PathParam("*")
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) ext, tp, err := archiver_service.ParseFileName(uri)
if err != nil {
ctx.ServerError("ParseFileName", err)
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
if err != nil { if err != nil {
ctx.ServerError("archiver_service.NewRequest", err) ctx.ServerError("archiver_service.NewRequest", err)
return return

View File

@ -322,6 +322,16 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
} }
func UpdateBranchProtectionPriories(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ProtectBranchPriorityForm)
repo := ctx.Repo.Repository
if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil {
ctx.ServerError("UpdateProtectBranchPriorities", err)
return
}
}
// RenameBranchPost responses for rename a branch // RenameBranchPost responses for rename a branch
func RenameBranchPost(ctx *context.Context) { func RenameBranchPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RenameBranchForm) form := web.GetForm(ctx).(*forms.RenameBranchForm)

View File

@ -31,6 +31,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues" issue_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -310,17 +311,14 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
ctx.Data["IsMarkup"] = true ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType ctx.Data["MarkupType"] = markupType
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
}).
WithMarkupType(markupType). WithMarkupType(markupType).
WithRelativePath(path.Join(ctx.Repo.TreePath, readmeFile.Name())). // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink, ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Join(ctx.Repo.TreePath, subfolder),
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil { if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
delete(ctx.Data, "IsMarkup") delete(ctx.Data, "IsMarkup")
@ -513,17 +511,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["MarkupType"] = markupType ctx.Data["MarkupType"] = markupType
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).
WithMarkupType(markupType). WithMarkupType(markupType).
WithRelativePath(ctx.Repo.TreePath). WithRelativePath(ctx.Repo.TreePath).
WithLinks(markup.Links{ WithMetas(metas)
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(), ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
TreePath: path.Dir(ctx.Repo.TreePath),
}).
WithMetas(metas).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
return return
@ -604,17 +600,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
rd := io.MultiReader(bytes.NewReader(buf), dataRc) rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType ctx.Data["MarkupType"] = markupType
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx).
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.BranchNameSubURL(),
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).
WithMarkupType(markupType). WithMarkupType(markupType).
WithRelativePath(ctx.Repo.TreePath). WithRelativePath(ctx.Repo.TreePath)
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink, ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
return return

View File

@ -14,6 +14,7 @@ import (
"strings" "strings"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
@ -288,11 +289,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
footerContent = data footerContent = data
} }
rctx := markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
WithMetas(ctx.Repo.Repository.ComposeWikiMetas(ctx)).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink})
buf := &strings.Builder{}
buf := &strings.Builder{}
renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) { renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
markupRd, markupWr := io.Pipe() markupRd, markupWr := io.Pipe()
defer markupWr.Close() defer markupWr.Close()

View File

@ -20,6 +20,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -27,7 +28,6 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues" issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -257,11 +257,8 @@ func Milestones(ctx *context.Context) {
continue continue
} }
milestones[i].RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoComment(ctx, milestones[i].Repo)
WithLinks(markup.Links{Base: milestones[i].Repo.Link()}). milestones[i].RenderedContent, err = markdown.RenderString(rctx, milestones[i].Content)
WithMetas(milestones[i].Repo.ComposeMetas(ctx)).
WithRepoFacade(milestones[i].Repo),
milestones[i].Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
return return

View File

@ -12,12 +12,12 @@ import (
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"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/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -72,17 +72,17 @@ func userProfile(ctx *context.Context) {
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
} }
profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
defer profileClose() defer profileClose()
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob) prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob)
// call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
shared_user.PrepareContextForProfileBigAvatar(ctx) shared_user.PrepareContextForProfileBigAvatar(ctx)
ctx.HTML(http.StatusOK, tplProfile) ctx.HTML(http.StatusOK, tplProfile)
} }
func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) { func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
// if there is not a profile readme, the overview tab should be treated as the repositories tab // if there is not a profile readme, the overview tab should be treated as the repositories tab
tab := ctx.FormString("tab") tab := ctx.FormString("tab")
@ -246,18 +246,10 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err) log.Error("failed to GetBlobContent: %v", err)
} else { } else {
if profileContent, err := markdown.RenderString(markup.NewRenderContext(ctx). rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
WithGitRepo(profileGitRepo). CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
WithLinks(markup.Links{ })
// Give the repo link to the markdown render for the full link of media element. if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// Eg. /Tom/.profile/media/branch/main
// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
// https://docs.gitea.com/usage/profile-readme
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
}),
bytes); err != nil {
log.Error("failed to RenderString: %v", err) log.Error("failed to RenderString: %v", err)
} else { } else {
ctx.Data["ProfileReadme"] = profileContent ctx.Data["ProfileReadme"] = profileContent

Some files were not shown because too many files have changed in this diff Show More