Skip to content

Commit

Permalink
Decouple the different contexts from each other (#24786)
Browse files Browse the repository at this point in the history
Replace #16455

Close #21803

Mixing different Gitea contexts together causes some problems:

1. Unable to respond proper content when error occurs, eg: Web should
respond HTML while API should respond JSON
2. Unclear dependency, eg: it's unclear when Context is used in
APIContext, which fields should be initialized, which methods are
necessary.


To make things clear, this PR introduces a Base context, it only
provides basic Req/Resp/Data features.

This PR mainly moves code. There are still many legacy problems and
TODOs in code, leave unrelated changes to future PRs.
  • Loading branch information
wxiaoguang committed May 21, 2023
1 parent 6ba4f89 commit 6b33152
Show file tree
Hide file tree
Showing 57 changed files with 882 additions and 778 deletions.
115 changes: 83 additions & 32 deletions modules/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,32 @@ import (

"code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
mc "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"

"gitea.com/go-chi/cache"
)

// APIContext is a specific context for API service
type APIContext struct {
*Context
Org *APIOrganization
*Base

Cache cache.Cache

Doer *user_model.User // current signed-in user
IsSigned bool
IsBasicAuth bool

ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer

Repo *Repository
Org *APIOrganization
Package *Package
}

// Currently, we have the following common fields in error response:
Expand Down Expand Up @@ -128,11 +142,6 @@ type apiContextKeyType struct{}

var apiContextKey = apiContextKeyType{}

// WithAPIContext set up api context in request
func WithAPIContext(req *http.Request, ctx *APIContext) *http.Request {
return req.WithContext(context.WithValue(req.Context(), apiContextKey, ctx))
}

// GetAPIContext returns a context for API routes
func GetAPIContext(req *http.Request) *APIContext {
return req.Context().Value(apiContextKey).(*APIContext)
Expand Down Expand Up @@ -195,21 +204,21 @@ func (ctx *APIContext) CheckForOTP() {
}

otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
twofa, err := auth.GetTwoFactorByUID(ctx.Context.Doer.ID)
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
return // No 2FA enrollment for this user
}
ctx.Context.Error(http.StatusInternalServerError)
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
return
}
ok, err := twofa.ValidateTOTP(otpHeader)
if err != nil {
ctx.Context.Error(http.StatusInternalServerError)
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
return
}
if !ok {
ctx.Context.Error(http.StatusUnauthorized)
ctx.Error(http.StatusUnauthorized, "", nil)
return
}
}
Expand All @@ -218,23 +227,17 @@ func (ctx *APIContext) CheckForOTP() {
func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
locale := middleware.Locale(w, req)
ctx := APIContext{
Context: &Context{
Resp: NewResponse(w),
Data: middleware.GetContextData(req.Context()),
Locale: locale,
Cache: cache.GetCache(),
Repo: &Repository{
PullRequest: &PullRequest{},
},
Org: &Organization{},
},
Org: &APIOrganization{},
base, baseCleanUp := NewBaseContext(w, req)
ctx := &APIContext{
Base: base,
Cache: mc.GetCache(),
Repo: &Repository{PullRequest: &PullRequest{}},
Org: &APIOrganization{},
}
defer ctx.Close()
defer baseCleanUp()

ctx.Req = WithAPIContext(WithContext(req, ctx.Context), &ctx)
ctx.Base.AppendContextValue(apiContextKey, ctx)
ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })

// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
Expand All @@ -247,8 +250,6 @@ func APIContexter() func(http.Handler) http.Handler {
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

ctx.Data["Context"] = &ctx

next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
Expand Down Expand Up @@ -301,7 +302,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context
return func() {
// If it's been set to nil then assume someone else has closed it.
if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close()
_ = ctx.Repo.GitRepo.Close()
}
}
}
Expand Down Expand Up @@ -337,7 +338,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
}

var err error
refName := getRefName(ctx.Context, RepoRefAny)
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)

if ctx.Repo.GitRepo.IsBranchExist(refName) {
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
Expand Down Expand Up @@ -368,3 +369,53 @@ func RepoRefForAPI(next http.Handler) http.Handler {
next.ServeHTTP(w, req)
})
}

// HasAPIError returns true if error occurs in form validation.
func (ctx *APIContext) HasAPIError() bool {
hasErr, ok := ctx.Data["HasError"]
if !ok {
return false
}
return hasErr.(bool)
}

// GetErrMsg returns error message in form validation.
func (ctx *APIContext) GetErrMsg() string {
msg, _ := ctx.Data["ErrorMsg"].(string)
if msg == "" {
msg = "invalid form data"
}
return msg
}

// NotFoundOrServerError use error check function to determine if the error
// is about not found. It responds with 404 status code for not found error,
// or error context description for logging purpose of 500 server error.
func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
if errCheck(logErr) {
ctx.JSON(http.StatusNotFound, nil)
return
}
ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg)
}

// IsUserSiteAdmin returns true if current user is a site admin
func (ctx *APIContext) IsUserSiteAdmin() bool {
return ctx.IsSigned && ctx.Doer.IsAdmin
}

// IsUserRepoAdmin returns true if current user is admin in current repo
func (ctx *APIContext) IsUserRepoAdmin() bool {
return ctx.Repo.IsAdmin()
}

// IsUserRepoWriter returns true if current user has write privilege in current repo
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
for _, unitType := range unitTypes {
if ctx.Repo.CanWrite(unitType) {
return true
}
}

return false
}
Loading

0 comments on commit 6b33152

Please sign in to comment.