Merge branch 'main' into lunny/remove_num_watches

This commit is contained in:
Lunny Xiao 2024-11-22 20:46:34 -08:00
commit 726f204c87
81 changed files with 1315 additions and 823 deletions

View File

@ -63,3 +63,4 @@ Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
Yu Liu <1240335630@qq.com> (@HEREYUA)
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
hiifong <i@hiif.ong> (@hiifong)

View File

@ -1944,6 +1944,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY =
;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea
;;
@ -2688,6 +2695,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY =
;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea
;;

View File

@ -112,14 +112,12 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
}
var err error
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Repo: issue.Repo,
Links: markup.Links{
Base: issue.Repo.Link(),
},
Metas: issue.Repo.ComposeMetas(ctx),
}, comment.Content); err != nil {
rctx := markup.NewRenderContext(ctx).
WithRepoFacade(issue.Repo).
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
}
}

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"xorm.io/builder"
)
@ -83,3 +84,16 @@ func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode per
OrderBy("name").
Find(&teams)
}
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
teams := make([]*Team, 0, 5)
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
Join("INNER", "team_repo", "team_repo.team_id = team.id").
Join("INNER", "team_unit", "team_unit.team_id = team.id").
And("team_repo.org_id = ?", orgID).
And("team_repo.repo_id = ?", repoID).
And("team_unit.type = ?", unitType).
OrderBy("name").
Find(&teams)
}

View File

@ -0,0 +1,31 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization_test
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
assert.NoError(t, err)
if assert.Len(t, teams, 2) {
assert.EqualValues(t, 21, teams[0].ID)
assert.EqualValues(t, 22, teams[1].ID)
}
}

View File

@ -629,10 +629,7 @@ func (repo *Repository) CanEnableEditor() bool {
// DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
Ctx: ctx,
// Don't use Metas to speedup requests
}, repo.Description)
desc, err := markup.RenderDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
return template.HTML(markup.SanitizeDescription(repo.Description))

View File

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
)
@ -146,57 +145,6 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
return users, nil
}
// GetReviewers get all users can be requested to review:
// * for private repositories this returns all users that have read access or higher to the repository.
// * for public repositories this returns all users that have read access or higher to the repository,
// all repo watchers and all organization members.
// TODO: may be we should have a busy choice for users to block review request to them.
func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) {
// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
cond := builder.And(builder.Neq{"`user`.id": posterID}).
And(builder.Eq{"`user`.is_active": true})
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository:
// Anyone who can read the repository is a requestable reviewer
cond = cond.And(builder.In("`user`.id",
builder.Select("user_id").From("access").Where(
builder.Eq{"repo_id": repo.ID}.
And(builder.Gte{"mode": perm.AccessModeRead}),
),
))
if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID {
// as private *user* repos don't generate an entry in the `access` table,
// the owner of a private repo needs to be explicitly added.
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID})
}
} else {
// This is a "public" repository:
// Any user that has read access, is a watcher or organization member can be requested to review
cond = cond.And(builder.And(builder.In("`user`.id",
builder.Select("user_id").From("access").
Where(builder.Eq{"repo_id": repo.ID}.
And(builder.Gte{"mode": perm.AccessModeRead})),
).Or(builder.In("`user`.id",
builder.Select("user_id").From("watch").
Where(builder.Eq{"repo_id": repo.ID}.
And(builder.In("mode", WatchModeNormal, WatchModeAuto))),
).Or(builder.In("`user`.id",
builder.Select("uid").From("org_user").
Where(builder.Eq{"org_id": repo.OwnerID}),
)))))
}
users := make([]*user_model.User, 0, 8)
return users, db.GetEngine(ctx).Where(cond).OrderBy(user_model.GetOrderByName()).Find(&users)
}
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
// If isShowFullName is set to true, also include full name prefix search
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {

View File

@ -38,46 +38,3 @@ func TestRepoAssignees(t *testing.T) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
}
}
func TestRepoGetReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// test public repo
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
ctx := db.DefaultContext
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
assert.NoError(t, err)
if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
}
// should include doer if doer is not PR poster.
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 3)
// should not include PR poster, if PR poster would be otherwise eligible
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 2)
// test private user repo
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
assert.EqualValues(t, reviewers[0].ID, 2)
// test private org repo
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1)
assert.NoError(t, err)
assert.Len(t, reviewers, 2)
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
}

View File

@ -7,7 +7,7 @@ import (
"bytes"
stdcsv "encoding/csv"
"io"
"path/filepath"
"path"
"regexp"
"strings"
@ -53,7 +53,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
extension := ".csv"
if ctx != nil {
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
extension = strings.ToLower(path.Ext(ctx.RenderOptions.RelativePath))
}
var delimiter rune

View File

@ -5,13 +5,13 @@ package csv
import (
"bytes"
"context"
"encoding/csv"
"io"
"strconv"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/translation"
@ -231,10 +231,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimiters`,
}
for n, c := range cases {
delimiter := determineDelimiter(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: c.filename,
}, []byte(decodeSlashes(t, c.csv)))
delimiter := determineDelimiter(markup.NewRenderContext(context.Background()).WithRelativePath(c.filename), []byte(decodeSlashes(t, c.csv)))
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
}
}

View File

@ -44,10 +44,10 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
setting.AppSubURL,
url.PathEscape(ctx.Metas["user"]),
url.PathEscape(ctx.Metas["repo"]),
ctx.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RelativePath),
url.PathEscape(ctx.RenderOptions.Metas["user"]),
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
ctx.RenderOptions.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RenderOptions.RelativePath),
)
return ctx.RenderInternal.FormatWithSafeAttrs(output, `<div class="%s" %s="%s"></div>`, playerClassName, playerSrcAttr, rawURL)
}

View File

@ -4,10 +4,10 @@
package console
import (
"context"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"github.com/stretchr/testify/assert"
@ -24,8 +24,7 @@ func TestRenderConsole(t *testing.T) {
canRender := render.CanRender("test", strings.NewReader(k))
assert.True(t, canRender)
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
strings.NewReader(k), &buf)
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
assert.NoError(t, err)
assert.EqualValues(t, v, buf.String())
}

View File

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

View File

@ -4,10 +4,10 @@
package markup
import (
"context"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"github.com/stretchr/testify/assert"
@ -24,8 +24,7 @@ func TestRenderCSV(t *testing.T) {
for k, v := range kases {
var buf strings.Builder
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
strings.NewReader(k), &buf)
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
assert.NoError(t, err)
assert.EqualValues(t, v, buf.String())
}

View File

@ -12,7 +12,6 @@ import (
"runtime"
"strings"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/process"
@ -80,8 +79,8 @@ func envMark(envName string) string {
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
var (
command = strings.NewReplacer(
envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
envMark("GITEA_PREFIX_SRC"), ctx.RenderOptions.Links.SrcLink(),
envMark("GITEA_PREFIX_RAW"), ctx.RenderOptions.Links.RawLink(),
).Replace(p.Command)
commands = strings.Fields(command)
args = commands[1:]
@ -113,22 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
args = append(args, f.Name())
}
if ctx.Ctx == nil {
if !setting.IsProd || setting.IsInTesting {
panic("RenderContext did not provide context")
}
log.Warn("RenderContext did not provide context, defaulting to Shutdown context")
ctx.Ctx = graceful.GetManager().ShutdownContext()
}
processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderOptions.Links.SrcLink()))
defer finished()
cmd := exec.CommandContext(processCtx, commands[0], args...)
cmd.Env = append(
os.Environ(),
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
"GITEA_PREFIX_SRC="+ctx.RenderOptions.Links.SrcLink(),
"GITEA_PREFIX_RAW="+ctx.RenderOptions.Links.RawLink(),
)
if !p.IsInputFile {
cmd.Stdin = input

View File

@ -38,7 +38,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
CommitID: node.Data[m[6]:m[7]],
FilePath: node.Data[m[8]:m[9]],
}
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, opts.FullURL) {
if !httplib.IsCurrentGiteaSiteURL(ctx, opts.FullURL) {
return 0, 0, "", nil
}
u, err := url.Parse(opts.FilePath)
@ -51,7 +51,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
opts.LineStart, opts.LineStop = lineStart, lineStop
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx, opts)
return m[0], m[1], h, err
}

View File

@ -9,7 +9,6 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@ -23,10 +22,7 @@ func TestRenderCodePreview(t *testing.T) {
},
})
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
MarkupType: markdown.MarkupName,
}, input)
buffer, err := markup.RenderString(markup.NewRenderContext(context.Background()).WithMarkupType(markdown.MarkupName), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}

View File

@ -84,7 +84,7 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
// fullHashPatternProcessor renders SHA containing URLs
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
if ctx.RenderOptions.Metas == nil {
return
}
nodeStop := node.NextSibling
@ -111,7 +111,7 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
}
func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
if ctx.RenderOptions.Metas == nil {
return
}
nodeStop := node.NextSibling
@ -163,14 +163,14 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
// are assumed to be in the same repository.
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || (ctx.Repo == nil && ctx.GitRepo == nil) {
if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || (ctx.RenderHelper.repoFacade == nil && ctx.RenderHelper.gitRepo == nil) {
return
}
start := 0
next := node.NextSibling
if ctx.ShaExistCache == nil {
ctx.ShaExistCache = make(map[string]bool)
if ctx.RenderHelper.shaExistCache == nil {
ctx.RenderHelper.shaExistCache = make(map[string]bool)
}
for node != nil && node != next && start < len(node.Data) {
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
@ -191,25 +191,25 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
// a commit in the repository before making it a link.
// check cache first
exist, inCache := ctx.ShaExistCache[hash]
exist, inCache := ctx.RenderHelper.shaExistCache[hash]
if !inCache {
if ctx.GitRepo == nil {
if ctx.RenderHelper.gitRepo == nil {
var err error
var closer io.Closer
ctx.GitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx.Ctx, ctx.Repo)
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.Repo), err)
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.RenderHelper.repoFacade), err)
return
}
ctx.AddCancel(func() {
_ = closer.Close()
ctx.GitRepo = nil
ctx.RenderHelper.gitRepo = nil
})
}
// Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
exist = ctx.GitRepo.IsReferenceExist(hash)
ctx.ShaExistCache[hash] = exist
exist = ctx.RenderHelper.gitRepo.IsReferenceExist(hash)
ctx.RenderHelper.shaExistCache[hash] = exist
}
if !exist {
@ -217,7 +217,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
continue
}
link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
link := util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
start = 0
node = node.NextSibling.NextSibling

View File

@ -4,12 +4,12 @@
package markup
import (
"context"
"fmt"
"strconv"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
testModule "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
@ -79,11 +79,11 @@ func TestRender_IssueIndexPattern(t *testing.T) {
// numeric: render inputs without valid mentions
test := func(s string) {
testRenderIssueIndexPattern(t, s, s, &RenderContext{
Ctx: git.DefaultContext,
ctx: context.Background(),
})
testRenderIssueIndexPattern(t, s, s, &RenderContext{
Ctx: git.DefaultContext,
Metas: numericMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: numericMetas},
})
}
@ -133,8 +133,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
}
expectedNil := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{
Ctx: git.DefaultContext,
Metas: localMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas},
})
class := "ref-issue"
@ -147,8 +147,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
}
expectedNum := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{
Ctx: git.DefaultContext,
Metas: numericMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: numericMetas},
})
}
@ -184,8 +184,8 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
// alphanumeric: render inputs without valid mentions
test := func(s string) {
testRenderIssueIndexPattern(t, s, s, &RenderContext{
Ctx: git.DefaultContext,
Metas: alphanumericMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: alphanumericMetas},
})
}
test("")
@ -217,8 +217,8 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
}
expected := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
Ctx: git.DefaultContext,
Metas: alphanumericMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: alphanumericMetas},
})
}
test("OTT-1234 test", "%s test", "OTT-1234")
@ -240,8 +240,8 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
expected := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
Ctx: git.DefaultContext,
Metas: metas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
}
@ -264,8 +264,8 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
)
testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{
Ctx: git.DefaultContext,
Metas: regexpMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: regexpMetas},
})
}
@ -279,16 +279,16 @@ func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
}
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
Ctx: git.DefaultContext,
Metas: metas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
testRenderIssueIndexPattern(t, "#1312", "#1312", &RenderContext{
Ctx: git.DefaultContext,
Metas: metas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
testRenderIssueIndexPattern(t, "!1", "!1", &RenderContext{
Ctx: git.DefaultContext,
Metas: metas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
})
}
@ -301,17 +301,17 @@ func TestRender_RenderIssueTitle(t *testing.T) {
"style": IssueNameStyleNumeric,
}
actual, err := RenderIssueTitle(&RenderContext{
Ctx: git.DefaultContext,
Metas: metas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: metas},
}, "#1")
assert.NoError(t, err)
assert.Equal(t, "#1", actual)
}
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
ctx.Links.AbsolutePrefix = true
if ctx.Links.Base == "" {
ctx.Links.Base = TestRepoURL
ctx.RenderOptions.Links.AbsolutePrefix = true
if ctx.RenderOptions.Links.Base == "" {
ctx.RenderOptions.Links.Base = TestRepoURL
}
var buf strings.Builder
@ -326,22 +326,18 @@ func TestRender_AutoLink(t *testing.T) {
test := func(input, expected string) {
var buffer strings.Builder
err := PostProcess(&RenderContext{
Ctx: git.DefaultContext,
Links: Links{
Base: TestRepoURL,
},
Metas: localMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
}, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
buffer.Reset()
err = PostProcess(&RenderContext{
Ctx: git.DefaultContext,
Links: Links{
Base: TestRepoURL,
},
Metas: localWikiMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localWikiMetas, Links: Links{Base: TestRepoURL}},
}, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
@ -368,11 +364,9 @@ func TestRender_FullIssueURLs(t *testing.T) {
test := func(input, expected string) {
var result strings.Builder
err := postProcess(&RenderContext{
Ctx: git.DefaultContext,
Links: Links{
Base: TestRepoURL,
},
Metas: localMetas,
ctx: context.Background(),
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
assert.NoError(t, err)
assert.Equal(t, expected, result.String())

View File

@ -19,7 +19,7 @@ import (
)
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
if ctx.RenderOptions.Metas == nil {
return
}
next := node.NextSibling
@ -36,14 +36,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
}
link := node.Data[m[0]:m[1]]
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) {
if !httplib.IsCurrentGiteaSiteURL(ctx, link) {
return
}
text := "#" + node.Data[m[2]:m[3]]
// if m[4] and m[5] is not -1, then link is to a comment
// indicate that in the text by appending (comment)
if m[4] != -1 && m[5] != -1 {
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
text += " " + locale.TrString("repo.from_comment")
} else {
text += " (comment)"
@ -56,7 +56,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
matchOrg := linkParts[len(linkParts)-4]
matchRepo := linkParts[len(linkParts)-3]
if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
if matchOrg == ctx.RenderOptions.Metas["user"] && matchRepo == ctx.RenderOptions.Metas["repo"] {
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
} else {
text = matchOrg + "/" + matchRepo + text
@ -67,14 +67,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
}
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
if ctx.RenderOptions.Metas == nil {
return
}
// crossLinkOnly: do not parse "#123", only parse "owner/repo#123"
// if there is no repo in the context, then the "#123" format can't be parsed
// old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true"
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
var (
found bool
@ -84,20 +84,20 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
for node != nil && node != next {
_, hasExtTrackFormat := ctx.Metas["format"]
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
// Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is
isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
switch ctx.Metas["style"] {
switch ctx.RenderOptions.Metas["style"] {
case "", IssueNameStyleNumeric:
found, ref = foundNumeric, refNumeric
case IssueNameStyleAlphanumeric:
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
case IssueNameStyleRegexp:
pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
if err != nil {
return
}
@ -121,9 +121,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
var link *html.Node
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if hasExtTrackFormat && !ref.IsPull {
ctx.Metas["index"] = ref.Issue
ctx.RenderOptions.Metas["index"] = ref.Issue
res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
if err != nil {
// here we could just log the error and continue the rendering
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
@ -136,9 +136,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Gitea will redirect on click as appropriate.
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
if ref.Owner == "" {
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
} else {
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
}
}
@ -177,7 +177,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
}
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
link := createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
link := createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling

View File

@ -19,15 +19,15 @@ import (
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
isAnchorFragment := link != "" && link[0] == '#'
if !isAnchorFragment && !IsFullURLString(link) {
linkBase := ctx.Links.Base
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.Links.WikiLink()
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
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.Links.SrcLink()
linkBase = ctx.RenderOptions.Links.SrcLink()
}
link, resolved = util.URLJoin(linkBase, link), true
}
@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
}
if image {
if !absoluteLink {
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
link = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
}
title := props["title"]
if title == "" {

View File

@ -25,15 +25,15 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
loc.Start += start
loc.End += start
mention := node.Data[loc.Start:loc.End]
teams, ok := ctx.Metas["teams"]
teams, ok := ctx.RenderOptions.Metas["teams"]
// FIXME: util.URLJoin may not be necessary here:
// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
// is an AppSubURL link we can probably fallback to concatenation.
// team mention should follow @orgName/teamName style
if ok && strings.Contains(mention, "/") {
mentionOrgAndTeam := strings.Split(mention, "/")
if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "" /*mention*/))
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*/))
node = node.NextSibling.NextSibling
start = 0
continue
@ -43,8 +43,8 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
}
mentionedUsername := mention[1:]
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "" /*mention*/))
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx, mentionedUsername) {
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), mentionedUsername), mention, "" /*mention*/))
node = node.NextSibling.NextSibling
start = 0
} else {

View File

@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
}
if IsNonEmptyRelativePath(attr.Val) {
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
// By default, the "<img>" tag should also be clickable,
// because frontend use `<img>` to paste the re-scaled image into the markdown,
@ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
continue
}
if IsNonEmptyRelativePath(attr.Val) {
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
}
attr.Val = camoHandleLink(attr.Val)
node.Attr[i] = attr

View File

@ -9,7 +9,6 @@ import (
"testing"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@ -57,16 +56,10 @@ func newMockRepo(ownerName, repoName string) gitrepo.Repository {
func TestRender_Commits(t *testing.T) {
setting.AppURL = markup.TestAppURL
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: ".md",
Links: markup.Links{
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas, newMockRepo(testRepoOwnerName, testRepoName), markup.Links{
AbsolutePrefix: true,
Base: markup.TestRepoURL,
},
Repo: newMockRepo(testRepoOwnerName, testRepoName),
Metas: localMetas,
}, input)
}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -112,15 +105,11 @@ func TestRender_CrossReferences(t *testing.T) {
setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas,
markup.Links{
AbsolutePrefix: true,
Base: setting.AppSubURL,
},
Metas: localMetas,
}, input)
}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -154,13 +143,7 @@ func TestRender_links(t *testing.T) {
setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
test := func(input, expected string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -265,13 +248,7 @@ func TestRender_email(t *testing.T) {
setting.AppURL = markup.TestAppURL
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
test := func(input, expected string) {
res, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
res, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
}
@ -338,13 +315,7 @@ func TestRender_emoji(t *testing.T) {
test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&amp;")
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: "a.md",
Links: markup.Links{
Base: markup.TestRepoURL,
},
}, input)
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -404,22 +375,10 @@ func TestRender_ShortLinks(t *testing.T) {
tree := util.URLJoin(markup.TestRepoURL, "src", "master")
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
BranchPath: "master",
},
}, input)
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: markup.TestRepoURL, BranchPath: "master"}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: markup.TestRepoURL,
},
Metas: localWikiMetas,
}, input)
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)))
}
@ -529,11 +488,7 @@ func TestRender_ShortLinks(t *testing.T) {
func TestRender_RelativeMedias(t *testing.T) {
render := func(input string, isWiki bool, links markup.Links) string {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: links,
Metas: util.Iif(isWiki, localWikiMetas, localMetas),
}, input)
buffer, err := markdown.RenderString(markup.NewTestRenderContext(links, util.Iif(isWiki, localWikiMetas, localMetas)), input)
assert.NoError(t, err)
return strings.TrimSpace(string(buffer))
}
@ -574,26 +529,14 @@ func Test_ParseClusterFuzz(t *testing.T) {
data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res)
err := markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html")
data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
res.Reset()
err = markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res)
err = markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html")
@ -606,14 +549,13 @@ func TestPostProcess_RenderDocument(t *testing.T) {
test := func(input, expected string) {
var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
err := markup.PostProcess(markup.NewTestRenderContext(
markup.Links{
AbsolutePrefix: true,
Base: "https://example.com",
},
Metas: map[string]string{"user": "go-gitea", "repo": "gitea"},
}, strings.NewReader(input), &res)
map[string]string{"user": "go-gitea", "repo": "gitea"},
), strings.NewReader(input), &res)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
}
@ -650,10 +592,7 @@ func TestIssue16020(t *testing.T) {
data := `<img src="data:image/png;base64,i//V"/>`
var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Metas: localMetas,
}, strings.NewReader(data), &res)
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(t, err)
assert.Equal(t, data, res.String())
}
@ -666,29 +605,23 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Metas: localMetas,
}, strings.NewReader(data), &res)
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
assert.NoError(b, err)
}
}
func TestFuzz(t *testing.T) {
s := "t/l/issues/8#/../../a"
renderContext := markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
renderContext := markup.NewTestRenderContext(
markup.Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{
map[string]string{
"user": "go-gitea",
"repo": "gitea",
},
}
err := markup.PostProcess(&renderContext, strings.NewReader(s), io.Discard)
)
err := markup.PostProcess(renderContext, strings.NewReader(s), io.Discard)
assert.NoError(t, err)
}
@ -696,10 +629,7 @@ func TestIssue18471(t *testing.T) {
data := `http://domain/org/repo/compare/783b039...da951ce`
var res strings.Builder
err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext,
Metas: localMetas,
}, strings.NewReader(data), &res)
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
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())

View File

@ -79,7 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link 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.
markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"]
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
v.SetHardLineBreak(true)
} else if markdownLineBreakStyle == "comment" {

View File

@ -182,7 +182,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
bufWithMetadataLength := len(buf)
rc := &RenderConfig{
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
Meta: markup.RenderMetaAsDetails,
Icon: "table",
Lang: "",
}
@ -241,7 +241,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
// Render renders Markdown to HTML with all specific handling stuff.
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
ctx.MarkupType = MarkupName
ctx.RenderOptions.MarkupType = MarkupName
return markup.Render(ctx, input, output)
}

View File

@ -4,12 +4,10 @@
package markdown_test
import (
"context"
"html/template"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@ -67,22 +65,11 @@ func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
}, input)
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
Metas: localWikiMetas,
}, input)
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)))
}
@ -101,12 +88,7 @@ func TestRender_Images(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
}, input)
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
}
@ -308,14 +290,11 @@ func TestTotal_RenderWiki(t *testing.T) {
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.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
Repo: newMockRepo(testRepoOwnerName, testRepoName),
Metas: localWikiMetas,
}, 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))
}
@ -334,13 +313,7 @@ func TestTotal_RenderWiki(t *testing.T) {
}
for i := 0; i < len(testCases); i += 2 {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
Metas: localWikiMetas,
}, testCases[i])
line, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}, localWikiMetas), testCases[i])
assert.NoError(t, err)
assert.EqualValues(t, testCases[i+1], string(line))
}
@ -352,15 +325,14 @@ func TestTotal_RenderString(t *testing.T) {
setting.AppURL = AppURL
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
line, err := markdown.RenderString(markup.NewTestRenderContext(
markup.Links{
Base: FullURL,
BranchPath: "master",
},
Repo: newMockRepo(testRepoOwnerName, testRepoName),
Metas: localMetas,
}, sameCases[i])
newMockRepo(testRepoOwnerName, testRepoName),
localMetas,
), sameCases[i])
assert.NoError(t, err)
assert.Equal(t, answers[i], string(line))
}
@ -368,12 +340,7 @@ func TestTotal_RenderString(t *testing.T) {
testCases := []string{}
for i := 0; i < len(testCases); i += 2 {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: FullURL,
},
}, testCases[i])
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)
}
@ -381,17 +348,17 @@ func TestTotal_RenderString(t *testing.T) {
func TestRender_RenderParagraphs(t *testing.T) {
test := func(t *testing.T, str string, cnt int) {
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, str)
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), str)
assert.NoError(t, err)
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
mac := strings.ReplaceAll(str, "\n", "\r")
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, mac)
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), mac)
assert.NoError(t, err)
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
dos := strings.ReplaceAll(str, "\n", "\r\n")
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, dos)
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), dos)
assert.NoError(t, err)
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
}
@ -419,7 +386,7 @@ func TestMarkdownRenderRaw(t *testing.T) {
for _, testcase := range testcases {
log.Info("Test markdown render error with fuzzy data: %x, the following errors can be recovered", testcase)
_, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, string(testcase))
_, err := markdown.RenderRawString(markup.NewTestRenderContext(), string(testcase))
assert.NoError(t, err)
}
}
@ -432,7 +399,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
<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.RenderContext{Ctx: git.DefaultContext}, testcase)
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
assert.NoError(t, err)
assert.Equal(t, expected, res)
}
@ -441,7 +408,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
testcase := `[Link with emoji :moon: in text](https://gitea.io)`
expected := `<p><a href="https://gitea.io" rel="nofollow">Link with emoji <span class="emoji" aria-label="waxing gibbous moon">🌔</span> in text</a></p>
`
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
assert.NoError(t, err)
assert.Equal(t, template.HTML(expected), res)
}
@ -479,7 +446,7 @@ func TestColorPreview(t *testing.T) {
}
for _, test := range positiveTests {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
@ -498,7 +465,7 @@ func TestColorPreview(t *testing.T) {
}
for _, test := range negativeTests {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test)
res, err := markdown.RenderString(markup.NewTestRenderContext(), test)
assert.NoError(t, err, "Unexpected error in testcase: %q", test)
assert.NotContains(t, res, `<span class="color-preview" style="background-color: `, "Unexpected result in testcase %q", test)
}
@ -573,7 +540,7 @@ func TestMathBlock(t *testing.T) {
}
for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
@ -610,7 +577,7 @@ foo: bar
}
for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
@ -1003,11 +970,7 @@ 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.RenderContext{
Ctx: context.Background(),
Links: c.Links,
Metas: util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{}),
}, input)
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)
}
@ -1029,7 +992,7 @@ func TestAttention(t *testing.T) {
}
test := func(input, expected string) {
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
}
@ -1062,6 +1025,6 @@ func BenchmarkSpecializedMarkdown(b *testing.B) {
func BenchmarkMarkdownRender(b *testing.B) {
// 23202 50840 ns/op
for i := 0; i < b.N; i++ {
_, _ = markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, "https://example.com\n- a\n- b\n")
_, _ = markdown.RenderString(markup.NewTestRenderContext(), "https://example.com\n- a\n- b\n")
}
}

View File

@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
// Check if the destination is a real link
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
v.Destination = []byte(giteautil.URLJoin(
ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
strings.TrimLeft(string(v.Destination), "/"),
))
}

View File

@ -143,15 +143,15 @@ func (r *Writer) resolveLink(kind, link string) string {
kind = org.RegularLink{URL: link}.Kind()
}
base := r.Ctx.Links.Base
base := r.Ctx.RenderOptions.Links.Base
if r.Ctx.IsMarkupContentWiki() {
base = r.Ctx.Links.WikiLink()
} else if r.Ctx.Links.HasBranchInfo() {
base = r.Ctx.Links.SrcLink()
base = r.Ctx.RenderOptions.Links.WikiLink()
} else if r.Ctx.RenderOptions.Links.HasBranchInfo() {
base = r.Ctx.RenderOptions.Links.SrcLink()
}
if kind == "image" || kind == "video" {
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
base = r.Ctx.RenderOptions.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
}
link = util.URLJoin(base, link)

View File

@ -4,10 +4,10 @@
package markup
import (
"os"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -15,20 +15,21 @@ import (
"github.com/stretchr/testify/assert"
)
const AppURL = "http://localhost:3000/"
func TestMain(m *testing.M) {
setting.AppURL = "http://localhost:3000/"
setting.IsInTesting = true
os.Exit(m.Run())
}
func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string, isWiki bool) {
buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
buffer, err := RenderString(markup.NewTestRenderContext(
markup.Links{
Base: "/relative-path",
BranchPath: "branch/main",
},
Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
}, input)
map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -42,16 +43,13 @@ func TestRender_StandardLinks(t *testing.T) {
}
func TestRender_InternalLinks(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
buffer, err := RenderString(markup.NewTestRenderContext(
markup.Links{
Base: "/relative-path",
BranchPath: "branch/main",
},
}, input)
), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -67,15 +65,8 @@ func TestRender_InternalLinks(t *testing.T) {
}
func TestRender_Media(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
Base: "./relative-path",
},
}, input)
buffer, err := RenderString(markup.NewTestRenderContext(markup.Links{Base: "./relative-path"}), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
@ -113,12 +104,8 @@ func TestRender_Media(t *testing.T) {
}
func TestRender_Source(t *testing.T) {
setting.AppURL = AppURL
test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
}, input)
buffer, err := RenderString(markup.NewTestRenderContext(), input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}

View File

@ -9,6 +9,7 @@ import (
"io"
"net/url"
"strings"
"time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
@ -42,16 +43,16 @@ var RenderBehaviorForTesting struct {
DisableInternalAttributes bool
}
// RenderContext represents a render context
type RenderContext struct {
Ctx context.Context
RelativePath string // relative path from tree root of the branch
type RenderOptions struct {
// relative path from tree root of the branch
RelativePath string
// eg: "orgmode", "asciicast", "console"
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
MarkupType string
Links Links // special link references for rendering, especially when there is a branch/tree path
// 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)
// BranchNameSubURL (for iframe&asciicast)
@ -59,27 +60,95 @@ type RenderContext struct {
// markdownLineBreakStyle (comment, document)
Metas map[string]string
GitRepo *git.Repository
Repo gitrepo.Repository
ShaExistCache map[string]bool
cancelFn func()
SidebarTocNode ast.Node
RenderMetaAs RenderMetaMode
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
InStandalonePage bool
}
type RenderHelper struct {
gitRepo *git.Repository
repoFacade gitrepo.Repository
shaExistCache map[string]bool
cancelFn func()
}
// RenderContext represents a render context
type RenderContext struct {
ctx context.Context
SidebarTocNode ast.Node
RenderHelper RenderHelper
RenderOptions RenderOptions
RenderInternal internal.RenderInternal
}
func (ctx *RenderContext) Deadline() (deadline time.Time, ok bool) {
return ctx.ctx.Deadline()
}
func (ctx *RenderContext) Done() <-chan struct{} {
return ctx.ctx.Done()
}
func (ctx *RenderContext) Err() error {
return ctx.ctx.Err()
}
func (ctx *RenderContext) Value(key any) any {
return ctx.ctx.Value(key)
}
var _ context.Context = (*RenderContext)(nil)
func NewRenderContext(ctx context.Context) *RenderContext {
return &RenderContext{ctx: ctx}
}
func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext {
ctx.RenderOptions.MarkupType = typ
return ctx
}
func (ctx *RenderContext) WithRelativePath(path string) *RenderContext {
ctx.RenderOptions.RelativePath = path
return ctx
}
func (ctx *RenderContext) WithLinks(links Links) *RenderContext {
ctx.RenderOptions.Links = links
return ctx
}
func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext {
ctx.RenderOptions.Metas = metas
return ctx
}
func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext {
ctx.RenderOptions.InStandalonePage = v
return ctx
}
func (ctx *RenderContext) WithGitRepo(r *git.Repository) *RenderContext {
ctx.RenderHelper.gitRepo = r
return ctx
}
func (ctx *RenderContext) WithRepoFacade(r gitrepo.Repository) *RenderContext {
ctx.RenderHelper.repoFacade = r
return ctx
}
// Cancel runs any cleanup functions that have been registered for this Ctx
func (ctx *RenderContext) Cancel() {
if ctx == nil {
return
}
ctx.ShaExistCache = map[string]bool{}
if ctx.cancelFn == nil {
ctx.RenderHelper.shaExistCache = map[string]bool{}
if ctx.RenderHelper.cancelFn == nil {
return
}
ctx.cancelFn()
ctx.RenderHelper.cancelFn()
}
// AddCancel adds the provided fn as a Cleanup for this Ctx
@ -87,38 +156,38 @@ func (ctx *RenderContext) AddCancel(fn func()) {
if ctx == nil {
return
}
oldCancelFn := ctx.cancelFn
oldCancelFn := ctx.RenderHelper.cancelFn
if oldCancelFn == nil {
ctx.cancelFn = fn
ctx.RenderHelper.cancelFn = fn
return
}
ctx.cancelFn = func() {
ctx.RenderHelper.cancelFn = func() {
defer oldCancelFn()
fn()
}
}
func (ctx *RenderContext) IsMarkupContentWiki() bool {
return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
return ctx.RenderOptions.Metas != nil && ctx.RenderOptions.Metas["markupContentMode"] == "wiki"
}
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
if ctx.MarkupType == "" && ctx.RelativePath != "" {
ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath)
if ctx.MarkupType == "" {
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath)
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
if ctx.RenderOptions.MarkupType == "" {
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
}
}
renderer := renderers[ctx.MarkupType]
renderer := renderers[ctx.RenderOptions.MarkupType]
if renderer == nil {
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType)
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
}
if ctx.RelativePath != "" {
if ctx.RenderOptions.RelativePath != "" {
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
if !ctx.InStandalonePage {
if !ctx.RenderOptions.InStandalonePage {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
@ -151,10 +220,10 @@ width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
sandbox="allow-scripts"
></iframe>`,
setting.AppSubURL,
url.PathEscape(ctx.Metas["user"]),
url.PathEscape(ctx.Metas["repo"]),
ctx.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RelativePath),
url.PathEscape(ctx.RenderOptions.Metas["user"]),
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
ctx.RenderOptions.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RenderOptions.RelativePath),
))
return err
}
@ -176,7 +245,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
pr1, pw1, close1 := pipes()
defer close1()
eg, _ := errgroup.WithContext(ctx.Ctx)
eg, _ := errgroup.WithContext(ctx)
var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
@ -230,3 +299,27 @@ func Init(ph *ProcessorHelper) {
func ComposeSimpleDocumentMetas() map[string]string {
return map[string]string{"markdownLineBreakStyle": "document"}
}
// NewTestRenderContext is a helper function to create a RenderContext for testing purpose
// It accepts string (RelativePath), Links, map[string]string (Metas), gitrepo.Repository
func NewTestRenderContext(a ...any) *RenderContext {
if !setting.IsInTesting {
panic("NewTestRenderContext should only be used in testing")
}
ctx := NewRenderContext(context.Background())
for _, v := range a {
switch v := v.(type) {
case string:
ctx = ctx.WithRelativePath(v)
case Links:
ctx = ctx.WithLinks(v)
case map[string]string:
ctx = ctx.WithMetas(v)
case gitrepo.Repository:
ctx = ctx.WithRepoFacade(v)
default:
panic(fmt.Sprintf("unknown type %T", v))
}
}
return ctx
}

View File

@ -43,6 +43,7 @@ type MinioStorageConfig struct {
Endpoint string `ini:"MINIO_ENDPOINT" json:",omitempty"`
AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID" json:",omitempty"`
SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY" json:",omitempty"`
IamEndpoint string `ini:"MINIO_IAM_ENDPOINT" json:",omitempty"`
Bucket string `ini:"MINIO_BUCKET" json:",omitempty"`
Location string `ini:"MINIO_LOCATION" json:",omitempty"`
BasePath string `ini:"MINIO_BASE_PATH" json:",omitempty"`

View File

@ -470,6 +470,19 @@ MINIO_BASE_PATH = /prefix
cfg, err = NewConfigProviderFromData(`
[storage]
STORAGE_TYPE = minio
MINIO_IAM_ENDPOINT = 127.0.0.1
MINIO_USE_SSL = true
MINIO_BASE_PATH = /prefix
`)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
assert.EqualValues(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint)
assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL)
assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
cfg, err = NewConfigProviderFromData(`
[storage]
STORAGE_TYPE = minio
MINIO_ACCESS_KEY_ID = my_access_key
MINIO_SECRET_ACCESS_KEY = my_secret_key
MINIO_USE_SSL = true

View File

@ -97,7 +97,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
}
minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
Creds: buildMinioCredentials(config),
Secure: config.UseSSL,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
Region: config.Location,
@ -164,7 +164,7 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
return p
}
func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string) *credentials.Credentials {
func buildMinioCredentials(config setting.MinioStorageConfig) *credentials.Credentials {
// If static credentials are provided, use those
if config.AccessKeyID != "" {
return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
@ -184,7 +184,9 @@ func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string
&credentials.FileAWSCredentials{},
// read IAM role from EC2 metadata endpoint if available
&credentials.IAM{
Endpoint: iamEndpoint,
// passing in an empty Endpoint lets the IAM Provider
// decide which endpoint to resolve internally
Endpoint: config.IamEndpoint,
Client: &http.Client{
Transport: http.DefaultTransport,
},

View File

@ -107,8 +107,9 @@ func TestMinioCredentials(t *testing.T) {
cfg := setting.MinioStorageConfig{
AccessKeyID: ExpectedAccessKey,
SecretAccessKey: ExpectedSecretAccessKey,
IamEndpoint: FakeEndpoint,
}
creds := buildMinioCredentials(cfg, FakeEndpoint)
creds := buildMinioCredentials(cfg)
v, err := creds.Get()
assert.NoError(t, err)
@ -117,13 +118,15 @@ func TestMinioCredentials(t *testing.T) {
})
t.Run("Chain", func(t *testing.T) {
cfg := setting.MinioStorageConfig{}
cfg := setting.MinioStorageConfig{
IamEndpoint: FakeEndpoint,
}
t.Run("EnvMinio", func(t *testing.T) {
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
creds := buildMinioCredentials(cfg, FakeEndpoint)
creds := buildMinioCredentials(cfg)
v, err := creds.Get()
assert.NoError(t, err)
@ -135,7 +138,7 @@ func TestMinioCredentials(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
creds := buildMinioCredentials(cfg, FakeEndpoint)
creds := buildMinioCredentials(cfg)
v, err := creds.Get()
assert.NoError(t, err)
@ -144,11 +147,11 @@ func TestMinioCredentials(t *testing.T) {
})
t.Run("FileMinio", func(t *testing.T) {
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
// prevent loading any actual credentials files from the user
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
creds := buildMinioCredentials(cfg, FakeEndpoint)
creds := buildMinioCredentials(cfg)
v, err := creds.Get()
assert.NoError(t, err)
@ -161,7 +164,7 @@ func TestMinioCredentials(t *testing.T) {
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
creds := buildMinioCredentials(cfg, FakeEndpoint)
creds := buildMinioCredentials(cfg)
v, err := creds.Get()
assert.NoError(t, err)
@ -187,7 +190,9 @@ func TestMinioCredentials(t *testing.T) {
defer server.Close()
// Use the provided EC2 Instance Metadata server
creds := buildMinioCredentials(cfg, server.URL)
creds := buildMinioCredentials(setting.MinioStorageConfig{
IamEndpoint: server.URL,
})
v, err := creds.Get()
assert.NoError(t, err)

View File

@ -38,10 +38,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string)
cleanMsg := template.HTMLEscapeString(msg)
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ut.ctx,
Metas: metas,
}, cleanMsg)
fullMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg)
if err != nil {
log.Error("RenderCommitMessage: %v", err)
return ""
@ -68,10 +65,7 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
Ctx: ut.ctx,
Metas: metas,
}, urlDefault, template.HTMLEscapeString(msgLine))
renderedMessage, err := markup.RenderCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine))
if err != nil {
log.Error("RenderCommitMessageSubject: %v", err)
return ""
@ -93,10 +87,7 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
return ""
}
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ut.ctx,
Metas: metas,
}, template.HTMLEscapeString(msgLine))
renderedMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine))
if err != nil {
log.Error("RenderCommitMessage: %v", err)
return ""
@ -115,10 +106,7 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
// RenderIssueTitle renders issue/pull title with defined post processors
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
Ctx: ut.ctx,
Metas: metas,
}, template.HTMLEscapeString(text))
renderedText, err := markup.RenderIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text))
if err != nil {
log.Error("RenderIssueTitle: %v", err)
return ""
@ -186,7 +174,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
// RenderEmoji renders html text with emoji post processors
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ut.ctx}, template.HTMLEscapeString(text))
renderedText, err := markup.RenderEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
if err != nil {
log.Error("RenderEmoji: %v", err)
return ""
@ -208,10 +196,7 @@ func reactionToEmoji(reaction string) template.HTML {
}
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive
output, err := markdown.RenderString(&markup.RenderContext{
Ctx: ut.ctx,
Metas: markup.ComposeSimpleDocumentMetas(),
}, input)
output, err := markdown.RenderString(markup.NewRenderContext(ut.ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), input)
if err != nil {
log.Error("RenderString: %v", err)
}

View File

@ -459,6 +459,7 @@ authorize_application = Authorize Application
authorize_redirect_notice = You will be redirected to %s if you authorize this application.
authorize_application_created_by = This application was created by %s.
authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations.
authorize_application_with_scopes = With scopes: %s
authorize_title = Authorize "%s" to access your account?
authorization_failed = Authorization failed
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize.

View File

@ -99,9 +99,7 @@ func MarkdownRaw(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
defer ctx.Req.Body.Close()
if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx,
}, ctx.Req.Body, ctx.Resp); err != nil {
if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil {
ctx.InternalServerError(err)
return
}

View File

@ -17,6 +17,8 @@ import (
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
)
@ -320,7 +322,13 @@ func GetReviewers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
reviewers, err := repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0)
canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0)
if !canChooseReviewer {
ctx.Error(http.StatusForbidden, "GetReviewers", errors.New("doer has no permission to get reviewers"))
return
}
reviewers, err := pull_service.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return

View File

@ -28,13 +28,12 @@ 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"
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
renderCtx := &markup.RenderContext{
Ctx: ctx,
Links: markup.Links{AbsolutePrefix: true},
MarkupType: markdown.MarkupName,
}
renderCtx := markup.NewRenderContext(ctx).
WithLinks(markup.Links{AbsolutePrefix: true}).
WithMarkupType(markdown.MarkupName)
if urlPathContext != "" {
renderCtx.Links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
renderCtx.RenderOptions.Links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
}
if mode == "" || mode == "markdown" {
@ -47,15 +46,14 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
switch mode {
case "gfm": // legacy mode, do nothing
case "comment":
renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "comment"}
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "comment"})
case "wiki":
renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"}
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"})
case "file":
// render the repo file content by its extension
renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document"}
renderCtx.MarkupType = ""
renderCtx.RelativePath = filePath
renderCtx.InStandalonePage = true
renderCtx = renderCtx.WithMetas(map[string]string{"markdownLineBreakStyle": "document"}).
WithMarkupType("").
WithRelativePath(filePath)
default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
return
@ -70,17 +68,17 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
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.Links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
renderCtx = renderCtx.WithLinks(markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir})
}
if repo != nil && repo.Repository != nil {
renderCtx.Repo = repo.Repository
renderCtx = renderCtx.WithRepoFacade(repo.Repository)
if mode == "file" {
renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx)
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeDocumentMetas(ctx))
} else if mode == "wiki" {
renderCtx.Metas = repo.Repository.ComposeWikiMetas(ctx)
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeWikiMetas(ctx))
} else if mode == "comment" {
renderCtx.Metas = repo.Repository.ComposeMetas(ctx)
renderCtx = renderCtx.WithMetas(repo.Repository.ComposeMetas(ctx))
}
}
if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil {

View File

@ -104,7 +104,18 @@ func InfoOAuth(ctx *context.Context) {
Picture: ctx.Doer.AvatarLink(ctx),
}
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
var accessTokenScope auth.AccessTokenScope
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
auths := strings.Fields(auHead)
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, auths[1])
}
}
// since version 1.22 does not verify if groups should be public-only,
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
if err != nil {
ctx.ServerError("Oauth groups for user", err)
return
@ -304,6 +315,9 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}
// check if additional scopes
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
// show authorize page to grant access
ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI

View File

@ -51,16 +51,14 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// renderMarkdown creates a minimal markdown render context from an action.
// If rendering fails, the original markdown text is returned
func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
markdownCtx := &markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
markdownCtx := markup.NewRenderContext(ctx).
WithLinks(markup.Links{
Base: act.GetRepoLink(ctx),
},
Metas: map[string]string{ // FIXME: not right here, it should use issue to compose the metas
}).
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
@ -296,14 +294,13 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (
}
link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Repo: rel.Repo,
Links: markup.Links{
content, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithRepoFacade(rel.Repo).
WithLinks(markup.Links{
Base: rel.Repo.Link(),
},
Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note)
}).
WithMetas(rel.Repo.ComposeMetas(ctx)),
rel.Note)
if err != nil {
return nil, err
}

View File

@ -41,13 +41,10 @@ func showUserFeed(ctx *context.Context, formatType string) {
return
}
ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
Base: ctx.ContextUser.HTMLURL(),
},
Metas: markup.ComposeSimpleDocumentMetas(),
}, ctx.ContextUser.Description)
ctxUserDescription, err := markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.ContextUser.HTMLURL()}).
WithMetas(markup.ComposeSimpleDocumentMetas()),
ctx.ContextUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -180,17 +180,16 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err)
} else {
if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
GitRepo: profileGitRepo,
Links: markup.Links{
if profileContent, err := markdown.RenderString(markup.NewRenderContext(ctx).
WithGitRepo(profileGitRepo).
WithLinks(markup.Links{
// Pass repo link to markdown render for the full link of media elements.
// The profile of default branch would be shown.
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
Metas: markup.ComposeSimpleDocumentMetas(),
}, bytes); err != nil {
}).
WithMetas(markup.ComposeSimpleDocumentMetas()),
bytes); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
ctx.Data["ProfileReadme"] = profileContent

View File

@ -392,16 +392,15 @@ func Diff(ctx *context.Context) {
if err == nil {
ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
Links: markup.Links{
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(markup.NewRenderContext(ctx).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
}).
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 {
ctx.ServerError("RenderCommitMessage", err)
return

View File

@ -149,7 +149,7 @@ func setCsvCompareContext(ctx *context.Context) {
return csvReader, reader, err
}
baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseBlob)
baseReader, baseBlobCloser, err := csvReaderFromCommit(markup.NewRenderContext(ctx).WithRelativePath(diffFile.OldName), baseBlob)
if baseBlobCloser != nil {
defer baseBlobCloser.Close()
}
@ -161,7 +161,7 @@ func setCsvCompareContext(ctx *context.Context) {
return CsvDiffResult{nil, "unable to load file"}
}
headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headBlob)
headReader, headBlobCloser, err := csvReaderFromCommit(markup.NewRenderContext(ctx).WithRelativePath(diffFile.Name), headBlob)
if headBlobCloser != nil {
defer headBlobCloser.Close()
}

View File

@ -366,15 +366,12 @@ func UpdateIssueContent(ctx *context.Context) {
}
}
content, err := markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, issue.Content)
content, err := markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.FormString("context")}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -267,15 +267,12 @@ func UpdateCommentContent(ctx *context.Context) {
var renderedContent template.HTML
if comment.Content != "" {
renderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, comment.Content)
renderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.FormString("context")}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -19,7 +19,7 @@ import (
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
pull_service "code.gitea.io/gitea/services/pull"
)
type issueSidebarMilestoneData struct {
@ -186,7 +186,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
if d.Issue == nil {
data.CanChooseReviewer = true
} else {
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue)
data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue.PosterID)
}
}
@ -231,13 +231,13 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
if data.CanChooseReviewer {
var err error
reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
reviewers, err = pull_service.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
ctx.ServerError("GetReviewers", err)
return
}
teamReviewers, err = repo_service.GetReviewerTeams(ctx, repo)
teamReviewers, err = pull_service.GetReviewerTeams(ctx, repo)
if err != nil {
ctx.ServerError("GetReviewerTeams", err)
return

View File

@ -359,15 +359,12 @@ func ViewIssue(ctx *context.Context) {
}
}
ctx.Data["IssueWatch"] = iw
issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, issue.Content)
issue.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
@ -467,15 +464,14 @@ func ViewIssue(ctx *context.Context) {
comment.Issue = issue
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
comment.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, comment.Content)
}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
@ -550,15 +546,12 @@ func ViewIssue(ctx *context.Context) {
}
}
} else if comment.Type.HasContentSupport() {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, comment.Content)
comment.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -79,15 +79,12 @@ func Milestones(ctx *context.Context) {
}
}
for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, m.Content)
m.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
m.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
@ -268,15 +265,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
return
}
milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, milestone.Content)
milestone.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
milestone.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -92,15 +92,12 @@ func Projects(ctx *context.Context) {
}
for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, projects[i].Description)
projects[i].RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
projects[i].Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
@ -425,15 +422,12 @@ func ViewProject(ctx *context.Context) {
ctx.Data["SelectLabels"] = selectLabels
ctx.Data["AssigneeID"] = assigneeID
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, project.Description)
project.RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
project.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -114,15 +114,12 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
cacheUsers[r.PublisherID] = r.Publisher
}
r.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Repo: ctx.Repo.Repository,
Ctx: ctx,
}, r.Note)
r.RenderedNote, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink}).
WithMetas(ctx.Repo.Repository.ComposeMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithRepoFacade(ctx.Repo.Repository),
r.Note)
if err != nil {
return nil, err
}

View File

@ -56,18 +56,17 @@ func RenderFile(ctx *context.Context) {
return
}
err = markup.Render(&markup.RenderContext{
Ctx: ctx,
RelativePath: ctx.Repo.TreePath,
Links: markup.Links{
err = markup.Render(markup.NewRenderContext(ctx).
WithRelativePath(ctx.Repo.TreePath).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
InStandalonePage: true,
}, rd, ctx.Resp)
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo).
WithInStandalonePage(true),
rd, ctx.Resp)
if err != nil {
log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)

View File

@ -352,6 +352,9 @@ func Action(ctx *context.Context) {
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
}
// see the `hx-trigger="refreshUserCards ..."` comments in tmpl
ctx.RespHeader().Add("hx-trigger", "refreshUserCards")
switch ctx.PathParam(":action") {
case "watch", "unwatch", "star", "unstar":
// we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed

View File

@ -310,18 +310,17 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx,
MarkupType: markupType,
RelativePath: 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).
Links: markup.Links{
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx).
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).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Join(ctx.Repo.TreePath, subfolder),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
}, rd)
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
delete(ctx.Data, "IsMarkup")
@ -514,18 +513,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["MarkupType"] = markupType
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx,
MarkupType: markupType,
RelativePath: ctx.Repo.TreePath,
Links: markup.Links{
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx).
WithMarkupType(markupType).
WithRelativePath(ctx.Repo.TreePath).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: metas,
GitRepo: ctx.Repo.GitRepo,
}, rd)
}).
WithMetas(metas).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil {
ctx.ServerError("Render", err)
return
@ -606,18 +604,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx,
MarkupType: markupType,
RelativePath: ctx.Repo.TreePath,
Links: markup.Links{
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, markup.NewRenderContext(ctx).
WithMarkupType(markupType).
WithRelativePath(ctx.Repo.TreePath).
WithLinks(markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
}, rd)
}).
WithMetas(ctx.Repo.Repository.ComposeDocumentMetas(ctx)).
WithGitRepo(ctx.Repo.GitRepo),
rd)
if err != nil {
ctx.ServerError("Render", err)
return
@ -1126,8 +1123,6 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp
func Watchers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.watchers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
ctx.Data["PageIsWatchers"] = true
numWatchers, err := repo_model.CountRepoWatchers(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("CountRepoWatchers", err)
@ -1143,7 +1138,6 @@ func Watchers(ctx *context.Context) {
func Stars(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.stargazers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
ctx.Data["PageIsStargazers"] = true
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
}, tplWatchers)

View File

@ -288,13 +288,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
footerContent = data
}
rctx := &markup.RenderContext{
Ctx: ctx,
Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx),
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
}
rctx := markup.NewRenderContext(ctx).
WithMetas(ctx.Repo.Repository.ComposeWikiMetas(ctx)).
WithLinks(markup.Links{Base: ctx.Repo.RepoLink})
buf := &strings.Builder{}
renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {

View File

@ -49,10 +49,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
}
ctx.Data["OpenIDs"] = openIDs
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{
Metas: markup.ComposeSimpleDocumentMetas(),
Ctx: ctx,
}, ctx.ContextUser.Description)
content, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), ctx.ContextUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -257,14 +257,11 @@ func Milestones(ctx *context.Context) {
continue
}
milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: milestones[i].Repo.Link(),
},
Metas: milestones[i].Repo.ComposeMetas(ctx),
Ctx: ctx,
Repo: milestones[i].Repo,
}, milestones[i].Content)
milestones[i].RenderedContent, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithLinks(markup.Links{Base: milestones[i].Repo.Link()}).
WithMetas(milestones[i].Repo.ComposeMetas(ctx)).
WithRepoFacade(milestones[i].Repo),
milestones[i].Content)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -246,10 +246,9 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err)
} else {
if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
GitRepo: profileGitRepo,
Links: markup.Links{
if profileContent, err := markdown.RenderString(markup.NewRenderContext(ctx).
WithGitRepo(profileGitRepo).
WithLinks(markup.Links{
// Give the repo link to the markdown render for the full link of media element.
// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// Eg. /Tom/.profile/media/branch/main
@ -257,8 +256,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
// https://docs.gitea.com/usage/profile-readme
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
}, bytes); err != nil {
}),
bytes); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
ctx.Data["ProfileReadme"] = profileContent

View File

@ -77,8 +77,8 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
log.Trace("Basic Authorization: Attempting login with username as token")
}
// check oauth2 token
uid := CheckOAuthAccessToken(req.Context(), authToken)
// get oauth2 token's user's ID
_, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)

View File

@ -26,33 +26,35 @@ var (
_ Method = &OAuth2{}
)
// CheckOAuthAccessToken returns uid of user from oauth token
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
var accessTokenScope auth_model.AccessTokenScope
if !setting.OAuth2.Enabled {
return 0
return accessTokenScope, 0
}
// JWT tokens require a ".", if the token isn't like that, return early
if !strings.Contains(accessToken, ".") {
return 0
return accessTokenScope, 0
}
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
if err != nil {
log.Trace("oauth2.ParseToken: %v", err)
return 0
return accessTokenScope, 0
}
var grant *auth_model.OAuth2Grant
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
return 0
return accessTokenScope, 0
}
if token.Kind != oauth2_provider.KindAccessToken {
return 0
return accessTokenScope, 0
}
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
return 0
return accessTokenScope, 0
}
return grant.UserID
accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
return accessTokenScope, grant.UserID
}
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
@ -120,10 +122,10 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
}
// Otherwise, check if this is an OAuth access token
uid := CheckOAuthAccessToken(ctx, tokenSHA)
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
if uid != 0 {
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
store.GetData()["ApiTokenScope"] = accessTokenScope
}
return uid
}

View File

@ -259,9 +259,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
}, ctx.ContextUser.Description)
content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return

View File

@ -119,7 +119,7 @@ func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
return err
}
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
if isAdd {
if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
@ -178,7 +178,7 @@ func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
}
}
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID)
if isAdd {
if issue.Repo.IsPrivate {
@ -276,12 +276,12 @@ func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doe
}
// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool {
if repo.IsArchived {
return false
}
// The poster of the PR can change the reviewers
if doer.ID == issue.PosterID {
if doer.ID == posterID {
return true
}

View File

@ -219,15 +219,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
}
// This is the body of the new issue or comment, not the mail body
body, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Repo: ctx.Issue.Repo,
Links: markup.Links{
AbsolutePrefix: true,
Base: ctx.Issue.Repo.HTMLURL(),
},
Metas: ctx.Issue.Repo.ComposeMetas(ctx),
}, ctx.Content)
body, err := markdown.RenderString(markup.NewRenderContext(ctx).
WithRepoFacade(ctx.Issue.Repo).
WithLinks(markup.Links{AbsolutePrefix: true, Base: ctx.Issue.Repo.HTMLURL()}).
WithMetas(ctx.Issue.Repo.ComposeMetas(ctx)),
ctx.Content)
if err != nil {
return nil, err
}

View File

@ -56,14 +56,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
locale := translation.NewLocale(lang)
var err error
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Repo: rel.Repo,
Links: markup.Links{
Base: rel.Repo.HTMLURL(),
},
Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note)
rel.RenderedNote, err = markdown.RenderString(markup.NewRenderContext(ctx).
WithRepoFacade(rel.Repo).
WithLinks(markup.Links{Base: rel.Repo.HTMLURL()}).
WithMetas(rel.Repo.ComposeMetas(ctx)),
rel.Note)
if err != nil {
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)
return

View File

@ -6,6 +6,8 @@ package oauth2_provider //nolint
import (
"context"
"fmt"
"slices"
"strings"
auth "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@ -69,6 +71,32 @@ type AccessTokenResponse struct {
IDToken string `json:"id_token,omitempty"`
}
// GrantAdditionalScopes returns valid scopes coming from grant
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
scopesSupported := []string{
"openid",
"profile",
"email",
"groups",
}
var tokenScopes []string
for _, tokenScope := range strings.Split(grantScopes, " ") {
if slices.Index(scopesSupported, tokenScope) == -1 {
tokenScopes = append(tokenScopes, tokenScope)
}
}
// since version 1.22, access tokens grant full access to the API
// with this access is reduced only if additional scopes are provided
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
return accessTokenWithAdditionalScopes
}
return auth.AccessTokenScopeAll
}
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil {
@ -161,7 +189,13 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
idToken.EmailVerified = user.IsActive
}
if grant.ScopeContains("groups") {
groups, err := GetOAuthGroupsForUser(ctx, user)
accessTokenScope := GrantAdditionalScopes(grant.Scope)
// since version 1.22 does not verify if groups should be public-only,
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
if err != nil {
log.Error("Error getting groups: %v", err)
return nil, &AccessTokenError{
@ -192,10 +226,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
// returns a list of "org" and "org:team" strings,
// that the given user is a part of.
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
UserID: user.ID,
IncludePrivate: true,
IncludePrivate: !onlyPublicGroups,
})
if err != nil {
return nil, fmt.Errorf("GetUserOrgList: %w", err)

View File

@ -0,0 +1,35 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2_provider //nolint
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGrantAdditionalScopes(t *testing.T) {
tests := []struct {
grantScopes string
expectedScopes string
}{
{"openid profile email", "all"},
{"openid profile email groups", "all"},
{"openid profile email all", "all"},
{"openid profile email read:user all", "all"},
{"openid profile email groups read:user", "read:user"},
{"read:user read:repository", "read:repository,read:user"},
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
{"openid profile email read:user", "read:user"},
{"read:invalid_scope", "all"},
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
}
for _, test := range tests {
t.Run(test.grantScopes, func(t *testing.T) {
result := GrantAdditionalScopes(test.grantScopes)
assert.Equal(t, test.expectedScopes, string(result))
})
}
}

89
services/pull/reviewer.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"xorm.io/builder"
)
// GetReviewers get all users can be requested to review:
// - Poster should not be listed
// - For collaborator, all users that have read access or higher to the repository.
// - For repository under organization, users under the teams which have read permission or higher of pull request unit
// - Owner will be listed if it's not an organization, not the poster and not in the list of reviewers
func GetReviewers(ctx context.Context, repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) {
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
e := db.GetEngine(ctx)
uniqueUserIDs := make(container.Set[int64])
collaboratorIDs := make([]int64, 0, 10)
if err := e.Table("collaboration").Where("repo_id=?", repo.ID).
And("mode >= ?", perm.AccessModeRead).
Select("user_id").
Find(&collaboratorIDs); err != nil {
return nil, err
}
uniqueUserIDs.AddMultiple(collaboratorIDs...)
if repo.Owner.IsOrganization() {
additionalUserIDs := make([]int64, 0, 10)
if err := e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? AND `team_unit`.`type` = ?)",
repo.ID, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uniqueUserIDs.AddMultiple(additionalUserIDs...)
}
uniqueUserIDs.Remove(posterID) // posterID should not be in the list of reviewers
// Leave a seat for owner itself to append later, but if owner is an organization
// and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
if len(uniqueUserIDs) > 0 {
if err := e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err
}
}
// add owner after all users are loaded because we can avoid load owner twice
if repo.OwnerID != posterID && !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(repo.OwnerID) {
users = append(users, repo.Owner)
}
return users, nil
}
// GetReviewerTeams get all teams can be requested to review
func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) {
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
if !repo.Owner.IsOrganization() {
return nil, nil
}
return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
}

View File

@ -0,0 +1,72 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull_test
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
pull_service "code.gitea.io/gitea/services/pull"
"github.com/stretchr/testify/assert"
)
func TestRepoGetReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// test public repo
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
ctx := db.DefaultContext
reviewers, err := pull_service.GetReviewers(ctx, repo1, 2, 0)
assert.NoError(t, err)
if assert.Len(t, reviewers, 1) {
assert.ElementsMatch(t, []int64{2}, []int64{reviewers[0].ID})
}
// should not include doer and remove the poster
reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 0)
// should not include PR poster, if PR poster would be otherwise eligible
reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
// test private user repo
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
assert.EqualValues(t, reviewers[0].ID, 2)
// test private org repo
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 1)
assert.NoError(t, err)
assert.Len(t, reviewers, 2)
reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
}
func TestRepoGetReviewerTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
teams, err := pull_service.GetReviewerTeams(db.DefaultContext, repo2)
assert.NoError(t, err)
assert.Empty(t, teams)
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
teams, err = pull_service.GetReviewerTeams(db.DefaultContext, repo3)
assert.NoError(t, err)
assert.Len(t, teams, 2)
}

View File

@ -1,24 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
)
// GetReviewerTeams get all teams can be requested to review
func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) {
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
if !repo.Owner.IsOrganization() {
return nil, nil
}
return organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
}

View File

@ -1,28 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"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"
)
func TestRepoGetReviewerTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
teams, err := GetReviewerTeams(db.DefaultContext, repo2)
assert.NoError(t, err)
assert.Empty(t, teams)
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
teams, err = GetReviewerTeams(db.DefaultContext, repo3)
assert.NoError(t, err)
assert.Len(t, teams, 2)
}

View File

@ -4,8 +4,8 @@
<div class="ui container">
{{template "repo/sub_menu" .}}
<div class="repo-button-row">
<div class="tw-flex tw-items-center">
{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
<div class="repo-button-row-left">
{{template "repo/branch_dropdown" dict "root" .}}
<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
{{svg "octicon-git-branch"}}
{{ctx.Locale.Tr "repo.commit_graph"}}

View File

@ -47,7 +47,7 @@
{{$isHomepage := (eq $n 0)}}
<div class="repo-button-row" data-is-homepage="{{$isHomepage}}">
<div class="repo-button-row-left">
{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
{{template "repo/branch_dropdown" dict "root" .}}
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}}
{{if ne .Repository.ID .BaseRepo.ID}}

View File

@ -6,10 +6,10 @@
{{$.CsrfTokenHtml}}
<button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}">
{{if not .Issue.IsPinned}}
{{svg "octicon-pin" 16 "tw-mr-2"}}
{{svg "octicon-pin"}}
{{ctx.Locale.Tr "pin"}}
{{else}}
{{svg "octicon-pin-slash" 16 "tw-mr-2"}}
{{svg "octicon-pin-slash"}}
{{ctx.Locale.Tr "unpin"}}
{{end}}
</button>

View File

@ -1,4 +1,14 @@
<div class="user-cards">
<!-- Refresh the content if a htmx response contains "HX-Trigger" header.
This usually happens when a user stays on the watchers/stargazers page
when they watched/unwatched/starred/unstarred and the list should be refreshed.
To test go to the watchers page and click the watch button. The user cards should reload.
At the moment, no JS initialization would re-trigger (fortunately there is no JS for this page).
-->
<div class="no-loading-indicator tw-hidden"></div>
<div class="user-cards"
hx-trigger="refreshUserCards from:body" hx-indicator=".no-loading-indicator"
hx-get="{{$.CurrentURL}}" hx-swap="outerHTML" hx-select=".user-cards"
>
{{if .CardsTitle}}
<h2 class="ui dividing header">
{{.CardsTitle}}

View File

@ -8,8 +8,11 @@
<div class="ui attached segment">
{{template "base/alert" .}}
<p>
{{if not .AdditionalScopes}}
<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
{{end}}
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br>
{{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}}
</p>
</div>
<div class="ui attached segment">

View File

@ -14,27 +14,22 @@ import (
"code.gitea.io/gitea/modules/setting"
)
var renderContext = markup.RenderContext{
Ctx: context.Background(),
Links: markup.Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{
"user": "go-gitea",
"repo": "gitea",
},
func newFuzzRenderContext() *markup.RenderContext {
return markup.NewRenderContext(context.Background()).
WithLinks(markup.Links{Base: "https://example.com/go-gitea/gitea"}).
WithMetas(map[string]string{"user": "go-gitea", "repo": "gitea"})
}
func FuzzMarkdownRenderRaw(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
setting.AppURL = "http://localhost:3000/"
markdown.RenderRaw(&renderContext, bytes.NewReader(data), io.Discard)
markdown.RenderRaw(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
})
}
func FuzzMarkupPostProcess(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
setting.AppURL = "http://localhost:3000/"
markup.PostProcess(&renderContext, bytes.NewReader(data), io.Discard)
markup.PostProcess(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
})
}

View File

@ -718,8 +718,8 @@ func TestAPIRepoGetReviewers(t *testing.T) {
resp := MakeRequest(t, req, http.StatusOK)
var reviewers []*api.User
DecodeJSON(t, resp, &reviewers)
if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
if assert.Len(t, reviewers, 1) {
assert.ElementsMatch(t, []int64{2}, []int64{reviewers[0].ID})
}
}

View File

@ -5,16 +5,25 @@ package integration
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthorizeNoClientID(t *testing.T) {
@ -477,3 +486,424 @@ func TestOAuthIntrospection(t *testing.T) {
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), "no valid authorization")
}
func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
},
ConfidentialClient: true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, resp, &app)
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: "openid read:user",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.Contains(t, grant.Scope, "openid read:user")
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 200)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
userReq := NewRequest(t, "GET", "/api/v1/user")
userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
userResp := MakeRequest(t, userReq, http.StatusOK)
type userResponse struct {
Login string `json:"login"`
Email string `json:"email"`
}
userParsed := new(userResponse)
require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), userParsed))
assert.Contains(t, userParsed.Email, "user2@example.com")
errorReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
type errorResponse struct {
Message string `json:"message"`
}
errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:repository]")
}
func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
},
ConfidentialClient: true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, resp, &app)
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: "openid read:user read:repository",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.Contains(t, grant.Scope, "openid read:user read:repository")
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
userReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
userResp := MakeRequest(t, userReq, http.StatusOK)
type repo struct {
FullRepoName string `json:"full_name"`
Private bool `json:"private"`
}
var reposCaptured []repo
require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), &reposCaptured))
reposExpected := []repo{
{
FullRepoName: "user2/repo1",
Private: false,
},
{
FullRepoName: "user2/repo2",
Private: true,
},
{
FullRepoName: "user2/repo15",
Private: true,
},
{
FullRepoName: "user2/repo16",
Private: true,
},
{
FullRepoName: "user2/repo20",
Private: true,
},
{
FullRepoName: "user2/utf8",
Private: false,
},
{
FullRepoName: "user2/commits_search_test",
Private: false,
},
{
FullRepoName: "user2/git_hooks_test",
Private: false,
},
{
FullRepoName: "user2/glob",
Private: false,
},
{
FullRepoName: "user2/lfs",
Private: true,
},
{
FullRepoName: "user2/scoped_label",
Private: true,
},
{
FullRepoName: "user2/readme-test",
Private: true,
},
{
FullRepoName: "user2/repo-release",
Private: false,
},
{
FullRepoName: "user2/commitsonpr",
Private: false,
},
{
FullRepoName: "user2/test_commit_revert",
Private: true,
},
}
assert.Equal(t, reposExpected, reposCaptured)
errorReq := NewRequest(t, "GET", "/api/v1/users/user2/orgs")
errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
type errorResponse struct {
Message string `json:"message"`
}
errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:user read:organization]")
}
func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
},
ConfidentialClient: true,
}
appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
appResp := MakeRequest(t, appReq, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, appResp, &app)
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: "openid groups read:user public-only",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"openid", "groups", "read:user", "public-only"}, strings.Split(grant.Scope, " "))
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
parts := strings.Split(parsed.IDToken, ".")
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
type IDTokenClaims struct {
Groups []string `json:"groups"`
}
claims := new(IDTokenClaims)
require.NoError(t, json.Unmarshal(payload, claims))
userinfoReq := NewRequest(t, "GET", "/login/oauth/userinfo")
userinfoReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
userinfoResp := MakeRequest(t, userinfoReq, http.StatusOK)
type userinfoResponse struct {
Login string `json:"login"`
Email string `json:"email"`
Groups []string `json:"groups"`
}
userinfoParsed := new(userinfoResponse)
require.NoError(t, json.Unmarshal(userinfoResp.Body.Bytes(), userinfoParsed))
assert.Contains(t, userinfoParsed.Email, "user2@example.com")
// test both id_token and call to /login/oauth/userinfo
for _, publicGroup := range []string{
"org17",
"org17:test_team",
"org3",
"org3:owners",
"org3:team1",
"org3:teamcreaterepo",
} {
assert.Contains(t, claims.Groups, publicGroup)
assert.Contains(t, userinfoParsed.Groups, publicGroup)
}
for _, privateGroup := range []string{
"private_org35",
"private_org35_team24",
} {
assert.NotContains(t, claims.Groups, privateGroup)
assert.NotContains(t, userinfoParsed.Groups, privateGroup)
}
}
func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
},
ConfidentialClient: true,
}
appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
appResp := MakeRequest(t, appReq, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, appResp, &app)
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: "openid groups",
}
err := db.Insert(db.DefaultContext, grant)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"openid", "groups"}, strings.Split(grant.Scope, " "))
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
parts := strings.Split(parsed.IDToken, ".")
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
type IDTokenClaims struct {
Groups []string `json:"groups"`
}
claims := new(IDTokenClaims)
require.NoError(t, json.Unmarshal(payload, claims))
userinfoReq := NewRequest(t, "GET", "/login/oauth/userinfo")
userinfoReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
userinfoResp := MakeRequest(t, userinfoReq, http.StatusOK)
type userinfoResponse struct {
Login string `json:"login"`
Email string `json:"email"`
Groups []string `json:"groups"`
}
userinfoParsed := new(userinfoResponse)
require.NoError(t, json.Unmarshal(userinfoResp.Body.Bytes(), userinfoParsed))
assert.Contains(t, userinfoParsed.Email, "user2@example.com")
// test both id_token and call to /login/oauth/userinfo
for _, group := range []string{
"org17",
"org17:test_team",
"org3",
"org3:owners",
"org3:team1",
"org3:teamcreaterepo",
"private_org35",
"private_org35:team24",
} {
assert.Contains(t, claims.Groups, group)
assert.Contains(t, userinfoParsed.Groups, group)
}
}