Decouple the different contexts from each other (#24786)
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.
This commit is contained in:
parent
6ba4f89723
commit
6b33152b7d
57 changed files with 885 additions and 781 deletions
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
|
300
modules/context/base.go
Normal file
300
modules/context/base.go
Normal file
|
@ -0,0 +1,300 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type contextValuePair struct {
|
||||
key any
|
||||
valueFn func() any
|
||||
}
|
||||
|
||||
type Base struct {
|
||||
originCtx context.Context
|
||||
contextValues []contextValuePair
|
||||
|
||||
Resp ResponseWriter
|
||||
Req *http.Request
|
||||
|
||||
// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData.
|
||||
// Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler
|
||||
Data middleware.ContextData
|
||||
|
||||
// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
|
||||
Locale translation.Locale
|
||||
}
|
||||
|
||||
func (b *Base) Deadline() (deadline time.Time, ok bool) {
|
||||
return b.originCtx.Deadline()
|
||||
}
|
||||
|
||||
func (b *Base) Done() <-chan struct{} {
|
||||
return b.originCtx.Done()
|
||||
}
|
||||
|
||||
func (b *Base) Err() error {
|
||||
return b.originCtx.Err()
|
||||
}
|
||||
|
||||
func (b *Base) Value(key any) any {
|
||||
for _, pair := range b.contextValues {
|
||||
if pair.key == key {
|
||||
return pair.valueFn()
|
||||
}
|
||||
}
|
||||
return b.originCtx.Value(key)
|
||||
}
|
||||
|
||||
func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any {
|
||||
b.contextValues = append(b.contextValues, contextValuePair{key, valueFn})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Base) AppendContextValue(key, value any) any {
|
||||
b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Base) GetData() middleware.ContextData {
|
||||
return b.Data
|
||||
}
|
||||
|
||||
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
|
||||
func (b *Base) AppendAccessControlExposeHeaders(names ...string) {
|
||||
val := b.RespHeader().Get("Access-Control-Expose-Headers")
|
||||
if len(val) != 0 {
|
||||
b.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
|
||||
} else {
|
||||
b.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// SetTotalCountHeader set "X-Total-Count" header
|
||||
func (b *Base) SetTotalCountHeader(total int64) {
|
||||
b.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
|
||||
b.AppendAccessControlExposeHeaders("X-Total-Count")
|
||||
}
|
||||
|
||||
// Written returns true if there are something sent to web browser
|
||||
func (b *Base) Written() bool {
|
||||
return b.Resp.Status() > 0
|
||||
}
|
||||
|
||||
// Status writes status code
|
||||
func (b *Base) Status(status int) {
|
||||
b.Resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write writes data to web browser
|
||||
func (b *Base) Write(bs []byte) (int, error) {
|
||||
return b.Resp.Write(bs)
|
||||
}
|
||||
|
||||
// RespHeader returns the response header
|
||||
func (b *Base) RespHeader() http.Header {
|
||||
return b.Resp.Header()
|
||||
}
|
||||
|
||||
// Error returned an error to web browser
|
||||
func (b *Base) Error(status int, contents ...string) {
|
||||
v := http.StatusText(status)
|
||||
if len(contents) > 0 {
|
||||
v = contents[0]
|
||||
}
|
||||
http.Error(b.Resp, v, status)
|
||||
}
|
||||
|
||||
// JSON render content as JSON
|
||||
func (b *Base) JSON(status int, content interface{}) {
|
||||
b.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||||
b.Resp.WriteHeader(status)
|
||||
if err := json.NewEncoder(b.Resp).Encode(content); err != nil {
|
||||
log.Error("Render JSON failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAddr returns the client machine ip address
|
||||
func (b *Base) RemoteAddr() string {
|
||||
return b.Req.RemoteAddr
|
||||
}
|
||||
|
||||
// Params returns the param on route
|
||||
func (b *Base) Params(p string) string {
|
||||
s, _ := url.PathUnescape(chi.URLParam(b.Req, strings.TrimPrefix(p, ":")))
|
||||
return s
|
||||
}
|
||||
|
||||
// ParamsInt64 returns the param on route as int64
|
||||
func (b *Base) ParamsInt64(p string) int64 {
|
||||
v, _ := strconv.ParseInt(b.Params(p), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// SetParams set params into routes
|
||||
func (b *Base) SetParams(k, v string) {
|
||||
chiCtx := chi.RouteContext(b)
|
||||
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
|
||||
}
|
||||
|
||||
// FormString returns the first value matching the provided key in the form as a string
|
||||
func (b *Base) FormString(key string) string {
|
||||
return b.Req.FormValue(key)
|
||||
}
|
||||
|
||||
// FormStrings returns a string slice for the provided key from the form
|
||||
func (b *Base) FormStrings(key string) []string {
|
||||
if b.Req.Form == nil {
|
||||
if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v, ok := b.Req.Form[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormTrim returns the first value for the provided key in the form as a space trimmed string
|
||||
func (b *Base) FormTrim(key string) string {
|
||||
return strings.TrimSpace(b.Req.FormValue(key))
|
||||
}
|
||||
|
||||
// FormInt returns the first value for the provided key in the form as an int
|
||||
func (b *Base) FormInt(key string) int {
|
||||
v, _ := strconv.Atoi(b.Req.FormValue(key))
|
||||
return v
|
||||
}
|
||||
|
||||
// FormInt64 returns the first value for the provided key in the form as an int64
|
||||
func (b *Base) FormInt64(key string) int64 {
|
||||
v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
|
||||
func (b *Base) FormBool(key string) bool {
|
||||
s := b.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return v
|
||||
}
|
||||
|
||||
// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
|
||||
// for the provided key exists in the form else it returns OptionalBoolNone
|
||||
func (b *Base) FormOptionalBool(key string) util.OptionalBool {
|
||||
value := b.Req.FormValue(key)
|
||||
if len(value) == 0 {
|
||||
return util.OptionalBoolNone
|
||||
}
|
||||
s := b.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return util.OptionalBoolOf(v)
|
||||
}
|
||||
|
||||
func (b *Base) SetFormString(key, value string) {
|
||||
_ = b.Req.FormValue(key) // force parse form
|
||||
b.Req.Form.Set(key, value)
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (b *Base) plainTextInternal(skip, status int, bs []byte) {
|
||||
statusPrefix := status / 100
|
||||
if statusPrefix == 4 || statusPrefix == 5 {
|
||||
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||||
}
|
||||
b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
b.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
b.Resp.WriteHeader(status)
|
||||
if _, err := b.Resp.Write(bs); err != nil {
|
||||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (b *Base) PlainTextBytes(status int, bs []byte) {
|
||||
b.plainTextInternal(2, status, bs)
|
||||
}
|
||||
|
||||
// PlainText renders content as plain text
|
||||
func (b *Base) PlainText(status int, text string) {
|
||||
b.plainTextInternal(2, status, []byte(text))
|
||||
}
|
||||
|
||||
// Redirect redirects the request
|
||||
func (b *Base) Redirect(location string, status ...int) {
|
||||
code := http.StatusSeeOther
|
||||
if len(status) == 1 {
|
||||
code = status[0]
|
||||
}
|
||||
|
||||
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
// 1. the first request to "/my-path" contains cookie
|
||||
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||||
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||||
// 4. then the browser accepts the empty session, then the user is logged out
|
||||
// So in this case, we should remove the session cookie from the response header
|
||||
removeSessionCookieHeader(b.Resp)
|
||||
}
|
||||
http.Redirect(b.Resp, b.Req, location, code)
|
||||
}
|
||||
|
||||
type ServeHeaderOptions httplib.ServeHeaderOptions
|
||||
|
||||
func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt))
|
||||
}
|
||||
|
||||
// ServeContent serves content to http request
|
||||
func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts))
|
||||
http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r)
|
||||
}
|
||||
|
||||
// Close frees all resources hold by Context
|
||||
func (b *Base) cleanUp() {
|
||||
if b.Req != nil && b.Req.MultipartForm != nil {
|
||||
_ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) Tr(msg string, args ...any) string {
|
||||
return b.Locale.Tr(msg, args...)
|
||||
}
|
||||
|
||||
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
|
||||
return b.Locale.TrN(cnt, key1, keyN, args...)
|
||||
}
|
||||
|
||||
func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) {
|
||||
b = &Base{
|
||||
originCtx: req.Context(),
|
||||
Req: req,
|
||||
Resp: WrapResponseWriter(resp),
|
||||
Locale: middleware.Locale(resp, req),
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
}
|
||||
b.AppendContextValue(translation.ContextKey, b.Locale)
|
||||
b.Req = b.Req.WithContext(b)
|
||||
return b, b.cleanUp
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
|
@ -36,38 +35,27 @@ type Render interface {
|
|||
|
||||
// Context represents context of a request.
|
||||
type Context struct {
|
||||
Resp ResponseWriter
|
||||
Req *http.Request
|
||||
Render Render
|
||||
*Base
|
||||
|
||||
Data middleware.ContextData // data used by MVC templates
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
Render Render
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
|
||||
Locale translation.Locale
|
||||
Cache cache.Cache
|
||||
Csrf CSRFProtector
|
||||
Flash *middleware.Flash
|
||||
Session session.Store
|
||||
|
||||
Link string // current request URL (without query string)
|
||||
Doer *user_model.User
|
||||
Link string // current request URL (without query string)
|
||||
|
||||
Doer *user_model.User // current signed-in user
|
||||
IsSigned bool
|
||||
IsBasicAuth bool
|
||||
|
||||
ContextUser *user_model.User
|
||||
Repo *Repository
|
||||
Org *Organization
|
||||
Package *Package
|
||||
}
|
||||
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
||||
|
||||
// Close frees all resources hold by Context
|
||||
func (ctx *Context) Close() error {
|
||||
var err error
|
||||
if ctx.Req != nil && ctx.Req.MultipartForm != nil {
|
||||
err = ctx.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
}
|
||||
// TODO: close opened repo, and more
|
||||
return err
|
||||
Repo *Repository
|
||||
Org *Organization
|
||||
Package *Package
|
||||
}
|
||||
|
||||
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
|
||||
|
@ -80,55 +68,30 @@ func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
|
|||
return ctx.Locale.Tr(msg, trArgs...)
|
||||
}
|
||||
|
||||
func (ctx *Context) Tr(msg string, args ...any) string {
|
||||
return ctx.Locale.Tr(msg, args...)
|
||||
}
|
||||
|
||||
func (ctx *Context) TrN(cnt any, key1, keyN string, args ...any) string {
|
||||
return ctx.Locale.TrN(cnt, key1, keyN, args...)
|
||||
}
|
||||
|
||||
// Deadline is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
|
||||
return ctx.Req.Context().Deadline()
|
||||
}
|
||||
|
||||
// Done is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *Context) Done() <-chan struct{} {
|
||||
return ctx.Req.Context().Done()
|
||||
}
|
||||
|
||||
// Err is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *Context) Err() error {
|
||||
return ctx.Req.Context().Err()
|
||||
}
|
||||
|
||||
// Value is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *Context) Value(key interface{}) interface{} {
|
||||
if key == git.RepositoryContextKey && ctx.Repo != nil {
|
||||
return ctx.Repo.GitRepo
|
||||
}
|
||||
if key == translation.ContextKey && ctx.Locale != nil {
|
||||
return ctx.Locale
|
||||
}
|
||||
return ctx.Req.Context().Value(key)
|
||||
}
|
||||
|
||||
type contextKeyType struct{}
|
||||
|
||||
var contextKey interface{} = contextKeyType{}
|
||||
|
||||
// WithContext set up install context in request
|
||||
func WithContext(req *http.Request, ctx *Context) *http.Request {
|
||||
return req.WithContext(context.WithValue(req.Context(), contextKey, ctx))
|
||||
func GetContext(req *http.Request) *Context {
|
||||
ctx, _ := req.Context().Value(contextKey).(*Context)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// GetContext retrieves install context from request
|
||||
func GetContext(req *http.Request) *Context {
|
||||
if ctx, ok := req.Context().Value(contextKey).(*Context); ok {
|
||||
return ctx
|
||||
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
|
||||
type ValidateContext struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
// GetValidateContext gets a context for middleware form validation
|
||||
func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
|
||||
if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
|
||||
ctx = &ValidateContext{Base: ctxAPI.Base}
|
||||
} else if ctxWeb, ok := req.Context().Value(contextKey).(*Context); ok {
|
||||
ctx = &ValidateContext{Base: ctxWeb.Base}
|
||||
} else {
|
||||
panic("invalid context, expect either APIContext or Context")
|
||||
}
|
||||
return nil
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Contexter initializes a classic context for a request.
|
||||
|
@ -150,20 +113,17 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx := Context{
|
||||
Resp: NewResponse(resp),
|
||||
base, baseCleanUp := NewBaseContext(resp, req)
|
||||
ctx := &Context{
|
||||
Base: base,
|
||||
Cache: mc.GetCache(),
|
||||
Locale: middleware.Locale(resp, req),
|
||||
Link: setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/"),
|
||||
Render: rnd,
|
||||
Session: session.GetSession(req),
|
||||
Repo: &Repository{
|
||||
PullRequest: &PullRequest{},
|
||||
},
|
||||
Org: &Organization{},
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
Repo: &Repository{PullRequest: &PullRequest{}},
|
||||
Org: &Organization{},
|
||||
}
|
||||
defer ctx.Close()
|
||||
defer baseCleanUp()
|
||||
|
||||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
||||
ctx.Data["Context"] = &ctx
|
||||
|
@ -175,15 +135,17 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageData"] = ctx.PageData
|
||||
|
||||
ctx.Req = WithContext(req, &ctx)
|
||||
ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx)
|
||||
ctx.Base.AppendContextValue(contextKey, ctx)
|
||||
ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
||||
|
||||
ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx)
|
||||
|
||||
// Get the last flash message from cookie
|
||||
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
||||
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
||||
// store last Flash message into the template data, to render it
|
||||
ctx.Data["Flash"] = &middleware.Flash{
|
||||
DataStore: &ctx,
|
||||
DataStore: ctx,
|
||||
Values: vals,
|
||||
ErrorMsg: vals.Get("error"),
|
||||
SuccessMsg: vals.Get("success"),
|
||||
|
@ -193,7 +155,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
}
|
||||
|
||||
// prepare an empty Flash message for current request
|
||||
ctx.Flash = &middleware.Flash{DataStore: &ctx, Values: url.Values{}}
|
||||
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
||||
ctx.Resp.Before(func(resp ResponseWriter) {
|
||||
if val := ctx.Flash.Encode(); val != "" {
|
||||
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0)
|
||||
|
@ -235,3 +197,24 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HasError returns true if error occurs in form validation.
|
||||
// Attention: this function changes ctx.Data and ctx.Flash
|
||||
func (ctx *Context) HasError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
return hasErr.(bool)
|
||||
}
|
||||
|
||||
// GetErrMsg returns error message in form validation.
|
||||
func (ctx *Context) GetErrMsg() string {
|
||||
msg, _ := ctx.Data["ErrorMsg"].(string)
|
||||
if msg == "" {
|
||||
msg = "invalid form data"
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import "code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
// GetData returns the data
|
||||
func (ctx *Context) GetData() middleware.ContextData {
|
||||
return ctx.Data
|
||||
}
|
||||
|
||||
// HasAPIError returns true if error occurs in form validation.
|
||||
func (ctx *Context) HasAPIError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hasErr.(bool)
|
||||
}
|
||||
|
||||
// GetErrMsg returns error message
|
||||
func (ctx *Context) GetErrMsg() string {
|
||||
return ctx.Data["ErrorMsg"].(string)
|
||||
}
|
||||
|
||||
// HasError returns true if error occurs in form validation.
|
||||
// Attention: this function changes ctx.Data and ctx.Flash
|
||||
func (ctx *Context) HasError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
return hasErr.(bool)
|
||||
}
|
||||
|
||||
// HasValue returns true if value of given name exists.
|
||||
func (ctx *Context) HasValue(name string) bool {
|
||||
_, ok := ctx.Data[name]
|
||||
return ok
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// FormString returns the first value matching the provided key in the form as a string
|
||||
func (ctx *Context) FormString(key string) string {
|
||||
return ctx.Req.FormValue(key)
|
||||
}
|
||||
|
||||
// FormStrings returns a string slice for the provided key from the form
|
||||
func (ctx *Context) FormStrings(key string) []string {
|
||||
if ctx.Req.Form == nil {
|
||||
if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v, ok := ctx.Req.Form[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormTrim returns the first value for the provided key in the form as a space trimmed string
|
||||
func (ctx *Context) FormTrim(key string) string {
|
||||
return strings.TrimSpace(ctx.Req.FormValue(key))
|
||||
}
|
||||
|
||||
// FormInt returns the first value for the provided key in the form as an int
|
||||
func (ctx *Context) FormInt(key string) int {
|
||||
v, _ := strconv.Atoi(ctx.Req.FormValue(key))
|
||||
return v
|
||||
}
|
||||
|
||||
// FormInt64 returns the first value for the provided key in the form as an int64
|
||||
func (ctx *Context) FormInt64(key string) int64 {
|
||||
v, _ := strconv.ParseInt(ctx.Req.FormValue(key), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
|
||||
func (ctx *Context) FormBool(key string) bool {
|
||||
s := ctx.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return v
|
||||
}
|
||||
|
||||
// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
|
||||
// for the provided key exists in the form else it returns OptionalBoolNone
|
||||
func (ctx *Context) FormOptionalBool(key string) util.OptionalBool {
|
||||
value := ctx.Req.FormValue(key)
|
||||
if len(value) == 0 {
|
||||
return util.OptionalBoolNone
|
||||
}
|
||||
s := ctx.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return util.OptionalBoolOf(v)
|
||||
}
|
||||
|
||||
func (ctx *Context) SetFormString(key, value string) {
|
||||
_ = ctx.Req.FormValue(key) // force parse form
|
||||
ctx.Req.Form.Set(key, value)
|
||||
}
|
|
@ -6,36 +6,9 @@ package context
|
|||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// RemoteAddr returns the client machine ip address
|
||||
func (ctx *Context) RemoteAddr() string {
|
||||
return ctx.Req.RemoteAddr
|
||||
}
|
||||
|
||||
// Params returns the param on route
|
||||
func (ctx *Context) Params(p string) string {
|
||||
s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
|
||||
return s
|
||||
}
|
||||
|
||||
// ParamsInt64 returns the param on route as int64
|
||||
func (ctx *Context) ParamsInt64(p string) int64 {
|
||||
v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// SetParams set params into routes
|
||||
func (ctx *Context) SetParams(k, v string) {
|
||||
chiCtx := chi.RouteContext(ctx)
|
||||
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
|
||||
}
|
||||
|
||||
// UploadStream returns the request body or the first form file
|
||||
// Only form files need to get closed.
|
||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
|
||||
|
|
|
@ -16,49 +16,17 @@ import (
|
|||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// SetTotalCountHeader set "X-Total-Count" header
|
||||
func (ctx *Context) SetTotalCountHeader(total int64) {
|
||||
ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
|
||||
ctx.AppendAccessControlExposeHeaders("X-Total-Count")
|
||||
}
|
||||
|
||||
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
|
||||
func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) {
|
||||
val := ctx.RespHeader().Get("Access-Control-Expose-Headers")
|
||||
if len(val) != 0 {
|
||||
ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
|
||||
} else {
|
||||
ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// Written returns true if there are something sent to web browser
|
||||
func (ctx *Context) Written() bool {
|
||||
return ctx.Resp.Status() > 0
|
||||
}
|
||||
|
||||
// Status writes status code
|
||||
func (ctx *Context) Status(status int) {
|
||||
ctx.Resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write writes data to web browser
|
||||
func (ctx *Context) Write(bs []byte) (int, error) {
|
||||
return ctx.Resp.Write(bs)
|
||||
}
|
||||
|
||||
// RedirectToUser redirect to a differently-named user
|
||||
func RedirectToUser(ctx *Context, userName string, redirectUserID int64) {
|
||||
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
|
||||
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
ctx.Error(http.StatusInternalServerError, "unable to get user")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -211,69 +179,3 @@ func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bo
|
|||
}
|
||||
ctx.serverErrorInternal(logMsg, logErr)
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
|
||||
statusPrefix := status / 100
|
||||
if statusPrefix == 4 || statusPrefix == 5 {
|
||||
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
ctx.Resp.WriteHeader(status)
|
||||
if _, err := ctx.Resp.Write(bs); err != nil {
|
||||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (ctx *Context) PlainTextBytes(status int, bs []byte) {
|
||||
ctx.plainTextInternal(2, status, bs)
|
||||
}
|
||||
|
||||
// PlainText renders content as plain text
|
||||
func (ctx *Context) PlainText(status int, text string) {
|
||||
ctx.plainTextInternal(2, status, []byte(text))
|
||||
}
|
||||
|
||||
// RespHeader returns the response header
|
||||
func (ctx *Context) RespHeader() http.Header {
|
||||
return ctx.Resp.Header()
|
||||
}
|
||||
|
||||
// Error returned an error to web browser
|
||||
func (ctx *Context) Error(status int, contents ...string) {
|
||||
v := http.StatusText(status)
|
||||
if len(contents) > 0 {
|
||||
v = contents[0]
|
||||
}
|
||||
http.Error(ctx.Resp, v, status)
|
||||
}
|
||||
|
||||
// JSON render content as JSON
|
||||
func (ctx *Context) JSON(status int, content interface{}) {
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||||
ctx.Resp.WriteHeader(status)
|
||||
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
|
||||
ctx.ServerError("Render JSON failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect redirects the request
|
||||
func (ctx *Context) Redirect(location string, status ...int) {
|
||||
code := http.StatusSeeOther
|
||||
if len(status) == 1 {
|
||||
code = status[0]
|
||||
}
|
||||
|
||||
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
// 1. the first request to "/my-path" contains cookie
|
||||
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||||
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||||
// 4. then the browser accepts the empty session, then the user is logged out
|
||||
// So in this case, we should remove the session cookie from the response header
|
||||
removeSessionCookieHeader(ctx.Resp)
|
||||
}
|
||||
http.Redirect(ctx.Resp, ctx.Req, location, code)
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
)
|
||||
|
||||
type ServeHeaderOptions httplib.ServeHeaderOptions
|
||||
|
||||
func (ctx *Context) SetServeHeaders(opt *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opt))
|
||||
}
|
||||
|
||||
// ServeContent serves content to http request
|
||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opts))
|
||||
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
|
||||
}
|
|
@ -47,7 +47,7 @@ func GetOrganizationByParams(ctx *Context) {
|
|||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(orgName)
|
||||
if err == nil {
|
||||
RedirectToUser(ctx, orgName, redirectUserID)
|
||||
RedirectToUser(ctx.Base, orgName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
@ -16,7 +15,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// Package contains owner, access mode and optional the package descriptor
|
||||
|
@ -26,10 +24,16 @@ type Package struct {
|
|||
Descriptor *packages_model.PackageDescriptor
|
||||
}
|
||||
|
||||
type packageAssignmentCtx struct {
|
||||
*Base
|
||||
Doer *user_model.User
|
||||
ContextUser *user_model.User
|
||||
}
|
||||
|
||||
// PackageAssignment returns a middleware to handle Context.Package assignment
|
||||
func PackageAssignment() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
packageAssignment(ctx, func(status int, title string, obj interface{}) {
|
||||
errorFn := func(status int, title string, obj interface{}) {
|
||||
err, ok := obj.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%s", obj)
|
||||
|
@ -39,68 +43,72 @@ func PackageAssignment() func(ctx *Context) {
|
|||
} else {
|
||||
ctx.ServerError(title, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser}
|
||||
ctx.Package = packageAssignment(paCtx, errorFn)
|
||||
}
|
||||
}
|
||||
|
||||
// PackageAssignmentAPI returns a middleware to handle Context.Package assignment
|
||||
func PackageAssignmentAPI() func(ctx *APIContext) {
|
||||
return func(ctx *APIContext) {
|
||||
packageAssignment(ctx.Context, ctx.Error)
|
||||
paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser}
|
||||
ctx.Package = packageAssignment(paCtx, ctx.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func packageAssignment(ctx *Context, errCb func(int, string, interface{})) {
|
||||
ctx.Package = &Package{
|
||||
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, interface{})) *Package {
|
||||
pkg := &Package{
|
||||
Owner: ctx.ContextUser,
|
||||
}
|
||||
|
||||
var err error
|
||||
ctx.Package.AccessMode, err = determineAccessMode(ctx)
|
||||
pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "determineAccessMode", err)
|
||||
return
|
||||
return pkg
|
||||
}
|
||||
|
||||
packageType := ctx.Params("type")
|
||||
name := ctx.Params("name")
|
||||
version := ctx.Params("version")
|
||||
if packageType != "" && name != "" && version != "" {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.Type(packageType), name, version)
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
errCb(http.StatusNotFound, "GetVersionByNameAndVersion", err)
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, "GetVersionByNameAndVersion", err)
|
||||
}
|
||||
return
|
||||
return pkg
|
||||
}
|
||||
|
||||
ctx.Package.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv)
|
||||
pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "GetPackageDescriptor", err)
|
||||
return
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Context) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && ctx.Doer == nil {
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && doer == nil {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
if ctx.Doer != nil && !ctx.Doer.IsGhost() && (!ctx.Doer.IsActive || ctx.Doer.ProhibitLogin) {
|
||||
if doer != nil && !doer.IsGhost() && (!doer.IsActive || doer.ProhibitLogin) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
// TODO: ActionUser permission check
|
||||
accessMode := perm.AccessModeNone
|
||||
if ctx.Package.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(ctx.Package.Owner)
|
||||
if pkg.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(pkg.Owner)
|
||||
|
||||
if ctx.Doer != nil && !ctx.Doer.IsGhost() {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. If user is logged in, check all team packages permissions
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, ctx.Doer.ID)
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
|
@ -110,19 +118,19 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) {
|
|||
accessMode = perm
|
||||
}
|
||||
}
|
||||
} else if organization.HasOrgOrUserVisible(ctx, ctx.Package.Owner, ctx.Doer) {
|
||||
} else if organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) {
|
||||
// 2. If user is non-login, check if org is visible to non-login user
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
if ctx.Doer != nil && !ctx.Doer.IsGhost() {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. Check if user is package owner
|
||||
if ctx.Doer.ID == ctx.Package.Owner.ID {
|
||||
if doer.ID == pkg.Owner.ID {
|
||||
accessMode = perm.AccessModeOwner
|
||||
} else if ctx.Package.Owner.Visibility == structs.VisibleTypePublic || ctx.Package.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else if ctx.Package.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
}
|
||||
|
@ -131,19 +139,18 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) {
|
|||
}
|
||||
|
||||
// PackageContexter initializes a package context for a request.
|
||||
func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
func PackageContexter() func(next http.Handler) http.Handler {
|
||||
renderer := templates.HTMLRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx := Context{
|
||||
Resp: NewResponse(resp),
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
Render: rnd,
|
||||
base, baseCleanUp := NewBaseContext(resp, req)
|
||||
ctx := &Context{
|
||||
Base: base,
|
||||
Render: renderer, // it is still needed when rendering 500 page in a package handler
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
ctx.Req = WithContext(req, &ctx)
|
||||
defer baseCleanUp()
|
||||
|
||||
ctx.Base.AppendContextValue(contextKey, ctx)
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,13 +11,14 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// PrivateContext represents a context for private routes
|
||||
type PrivateContext struct {
|
||||
*Context
|
||||
*Base
|
||||
Override context.Context
|
||||
|
||||
Repo *Repository
|
||||
}
|
||||
|
||||
// Deadline is part of the interface for context.Context and we pass this to the request context
|
||||
|
@ -25,7 +26,7 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
|
|||
if ctx.Override != nil {
|
||||
return ctx.Override.Deadline()
|
||||
}
|
||||
return ctx.Req.Context().Deadline()
|
||||
return ctx.Base.Deadline()
|
||||
}
|
||||
|
||||
// Done is part of the interface for context.Context and we pass this to the request context
|
||||
|
@ -33,7 +34,7 @@ func (ctx *PrivateContext) Done() <-chan struct{} {
|
|||
if ctx.Override != nil {
|
||||
return ctx.Override.Done()
|
||||
}
|
||||
return ctx.Req.Context().Done()
|
||||
return ctx.Base.Done()
|
||||
}
|
||||
|
||||
// Err is part of the interface for context.Context and we pass this to the request context
|
||||
|
@ -41,16 +42,11 @@ func (ctx *PrivateContext) Err() error {
|
|||
if ctx.Override != nil {
|
||||
return ctx.Override.Err()
|
||||
}
|
||||
return ctx.Req.Context().Err()
|
||||
return ctx.Base.Err()
|
||||
}
|
||||
|
||||
var privateContextKey interface{} = "default_private_context"
|
||||
|
||||
// WithPrivateContext set up private context in request
|
||||
func WithPrivateContext(req *http.Request, ctx *PrivateContext) *http.Request {
|
||||
return req.WithContext(context.WithValue(req.Context(), privateContextKey, ctx))
|
||||
}
|
||||
|
||||
// GetPrivateContext returns a context for Private routes
|
||||
func GetPrivateContext(req *http.Request) *PrivateContext {
|
||||
return req.Context().Value(privateContextKey).(*PrivateContext)
|
||||
|
@ -60,16 +56,11 @@ func GetPrivateContext(req *http.Request) *PrivateContext {
|
|||
func PrivateContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := &PrivateContext{
|
||||
Context: &Context{
|
||||
Resp: NewResponse(w),
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
},
|
||||
}
|
||||
defer ctx.Close()
|
||||
base, baseCleanUp := NewBaseContext(w, req)
|
||||
ctx := &PrivateContext{Base: base}
|
||||
defer baseCleanUp()
|
||||
ctx.Base.AppendContextValue(privateContextKey, ctx)
|
||||
|
||||
ctx.Req = WithPrivateContext(req, ctx)
|
||||
ctx.Data["Context"] = ctx
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -331,13 +331,14 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
|
|||
}
|
||||
|
||||
// RedirectToRepo redirect to a differently-named repository
|
||||
func RedirectToRepo(ctx *Context, redirectRepoID int64) {
|
||||
func RedirectToRepo(ctx *Base, redirectRepoID int64) {
|
||||
ownerName := ctx.Params(":username")
|
||||
previousRepoName := ctx.Params(":reponame")
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
log.Error("GetRepositoryByID: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetRepositoryByID")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -456,7 +457,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
|
|||
}
|
||||
|
||||
if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
|
||||
RedirectToUser(ctx, userName, redirectUserID)
|
||||
RedirectToUser(ctx.Base, userName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.NotFound("GetUserByName", nil)
|
||||
} else {
|
||||
|
@ -498,7 +499,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
|
|||
if repo_model.IsErrRepoNotExist(err) {
|
||||
redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName)
|
||||
if err == nil {
|
||||
RedirectToRepo(ctx, redirectRepoID)
|
||||
RedirectToRepo(ctx.Base, redirectRepoID)
|
||||
} else if repo_model.IsErrRedirectNotExist(err) {
|
||||
if ctx.FormString("go-get") == "1" {
|
||||
EarlyResponseForGoGetMeta(ctx)
|
||||
|
@ -781,46 +782,46 @@ func (rt RepoRefType) RefTypeIncludesTags() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func getRefNameFromPath(ctx *Context, path string, isExist func(string) bool) string {
|
||||
func getRefNameFromPath(ctx *Base, repo *Repository, path string, isExist func(string) bool) string {
|
||||
refName := ""
|
||||
parts := strings.Split(path, "/")
|
||||
for i, part := range parts {
|
||||
refName = strings.TrimPrefix(refName+"/"+part, "/")
|
||||
if isExist(refName) {
|
||||
ctx.Repo.TreePath = strings.Join(parts[i+1:], "/")
|
||||
repo.TreePath = strings.Join(parts[i+1:], "/")
|
||||
return refName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getRefName(ctx *Context, pathType RepoRefType) string {
|
||||
func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
|
||||
path := ctx.Params("*")
|
||||
switch pathType {
|
||||
case RepoRefLegacy, RepoRefAny:
|
||||
if refName := getRefName(ctx, RepoRefBranch); len(refName) > 0 {
|
||||
if refName := getRefName(ctx, repo, RepoRefBranch); len(refName) > 0 {
|
||||
return refName
|
||||
}
|
||||
if refName := getRefName(ctx, RepoRefTag); len(refName) > 0 {
|
||||
if refName := getRefName(ctx, repo, RepoRefTag); len(refName) > 0 {
|
||||
return refName
|
||||
}
|
||||
// For legacy and API support only full commit sha
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) == git.SHAFullLength {
|
||||
ctx.Repo.TreePath = strings.Join(parts[1:], "/")
|
||||
repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
if refName := getRefName(ctx, RepoRefBlob); len(refName) > 0 {
|
||||
if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 {
|
||||
return refName
|
||||
}
|
||||
ctx.Repo.TreePath = path
|
||||
return ctx.Repo.Repository.DefaultBranch
|
||||
repo.TreePath = path
|
||||
return repo.Repository.DefaultBranch
|
||||
case RepoRefBranch:
|
||||
ref := getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist)
|
||||
ref := getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsBranchExist)
|
||||
if len(ref) == 0 {
|
||||
// maybe it's a renamed branch
|
||||
return getRefNameFromPath(ctx, path, func(s string) bool {
|
||||
b, exist, err := git_model.FindRenamedBranch(ctx, ctx.Repo.Repository.ID, s)
|
||||
return getRefNameFromPath(ctx, repo, path, func(s string) bool {
|
||||
b, exist, err := git_model.FindRenamedBranch(ctx, repo.Repository.ID, s)
|
||||
if err != nil {
|
||||
log.Error("FindRenamedBranch", err)
|
||||
return false
|
||||
|
@ -839,15 +840,15 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
|
|||
|
||||
return ref
|
||||
case RepoRefTag:
|
||||
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist)
|
||||
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
|
||||
case RepoRefCommit:
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength {
|
||||
ctx.Repo.TreePath = strings.Join(parts[1:], "/")
|
||||
repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
case RepoRefBlob:
|
||||
_, err := ctx.Repo.GitRepo.GetBlob(path)
|
||||
_, err := repo.GitRepo.GetBlob(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -922,7 +923,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
|||
}
|
||||
ctx.Repo.IsViewBranch = true
|
||||
} else {
|
||||
refName = getRefName(ctx, refType)
|
||||
refName = getRefName(ctx.Base, ctx.Repo, refType)
|
||||
ctx.Repo.RefName = refName
|
||||
isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
|
||||
if isRenamedBranch && has {
|
||||
|
|
|
@ -10,10 +10,9 @@ import (
|
|||
// ResponseWriter represents a response writer for HTTP
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
Flush()
|
||||
http.Flusher
|
||||
Status() int
|
||||
Before(func(ResponseWriter))
|
||||
Size() int
|
||||
}
|
||||
|
||||
var _ ResponseWriter = &Response{}
|
||||
|
@ -27,11 +26,6 @@ type Response struct {
|
|||
beforeExecuted bool
|
||||
}
|
||||
|
||||
// Size return written size
|
||||
func (r *Response) Size() int {
|
||||
return r.written
|
||||
}
|
||||
|
||||
// Write writes bytes to HTTP endpoint
|
||||
func (r *Response) Write(bs []byte) (int, error) {
|
||||
if !r.beforeExecuted {
|
||||
|
@ -65,7 +59,7 @@ func (r *Response) WriteHeader(statusCode int) {
|
|||
}
|
||||
}
|
||||
|
||||
// Flush flush cached data
|
||||
// Flush flushes cached data
|
||||
func (r *Response) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
|
@ -83,8 +77,7 @@ func (r *Response) Before(f func(ResponseWriter)) {
|
|||
r.befores = append(r.befores, f)
|
||||
}
|
||||
|
||||
// NewResponse creates a response
|
||||
func NewResponse(resp http.ResponseWriter) *Response {
|
||||
func WrapResponseWriter(resp http.ResponseWriter) *Response {
|
||||
if v, ok := resp.(*Response); ok {
|
||||
return v
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
|
||||
func GetQueryBeforeSince(ctx *Context) (before, since int64, err error) {
|
||||
func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) {
|
||||
qCreatedBefore, err := prepareQueryArg(ctx, "before")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
@ -48,7 +48,7 @@ func parseTime(value string) (int64, error) {
|
|||
}
|
||||
|
||||
// prepareQueryArg unescape and trim a query arg
|
||||
func prepareQueryArg(ctx *Context, name string) (value string, err error) {
|
||||
func prepareQueryArg(ctx *Base, name string) (value string, err error) {
|
||||
value, err = url.PathUnescape(ctx.FormString(name))
|
||||
value = strings.TrimSpace(value)
|
||||
return value, err
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
scontext "context"
|
||||
gocontext "context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -28,18 +28,7 @@ import (
|
|||
// MockContext mock context for unit tests
|
||||
// TODO: move this function to other packages, because it depends on "models" package
|
||||
func MockContext(t *testing.T, path string) *context.Context {
|
||||
resp := &mockResponseWriter{}
|
||||
ctx := context.Context{
|
||||
Render: &mockRender{},
|
||||
Data: make(middleware.ContextData),
|
||||
Flash: &middleware.Flash{
|
||||
Values: make(url.Values),
|
||||
},
|
||||
Resp: context.NewResponse(resp),
|
||||
Locale: &translation.MockLocale{},
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
requestURL, err := url.Parse(path)
|
||||
assert.NoError(t, err)
|
||||
req := &http.Request{
|
||||
|
@ -47,41 +36,105 @@ func MockContext(t *testing.T, path string) *context.Context {
|
|||
Form: url.Values{},
|
||||
}
|
||||
|
||||
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||
base.Data = middleware.ContextData{}
|
||||
base.Locale = &translation.MockLocale{}
|
||||
ctx := &context.Context{
|
||||
Base: base,
|
||||
Render: &mockRender{},
|
||||
Flash: &middleware.Flash{Values: url.Values{}},
|
||||
}
|
||||
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
req = req.WithContext(scontext.WithValue(req.Context(), chi.RouteCtxKey, chiCtx))
|
||||
ctx.Req = context.WithContext(req, &ctx)
|
||||
return &ctx
|
||||
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// MockAPIContext mock context for unit tests
|
||||
// TODO: move this function to other packages, because it depends on "models" package
|
||||
func MockAPIContext(t *testing.T, path string) *context.APIContext {
|
||||
resp := httptest.NewRecorder()
|
||||
requestURL, err := url.Parse(path)
|
||||
assert.NoError(t, err)
|
||||
req := &http.Request{
|
||||
URL: requestURL,
|
||||
Form: url.Values{},
|
||||
}
|
||||
|
||||
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||
base.Data = middleware.ContextData{}
|
||||
base.Locale = &translation.MockLocale{}
|
||||
ctx := &context.APIContext{Base: base}
|
||||
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// LoadRepo load a repo into a test context.
|
||||
func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) {
|
||||
ctx.Repo = &context.Repository{}
|
||||
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
|
||||
var doer *user_model.User
|
||||
repo := &context.Repository{}
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
ctx.Repo = repo
|
||||
doer = ctx.Doer
|
||||
case *context.APIContext:
|
||||
ctx.Repo = repo
|
||||
doer = ctx.Doer
|
||||
default:
|
||||
assert.Fail(t, "context is not *context.Context or *context.APIContext")
|
||||
return
|
||||
}
|
||||
|
||||
repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
var err error
|
||||
ctx.Repo.Owner, err = user_model.GetUserByID(ctx, ctx.Repo.Repository.OwnerID)
|
||||
repo.Owner, err = user_model.GetUserByID(ctx, repo.Repository.OwnerID)
|
||||
assert.NoError(t, err)
|
||||
ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
repo.RepoLink = repo.Repository.Link()
|
||||
repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo.Repository, doer)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// LoadRepoCommit loads a repo's commit into a test context.
|
||||
func LoadRepoCommit(t *testing.T, ctx *context.Context) {
|
||||
gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
|
||||
func LoadRepoCommit(t *testing.T, ctx gocontext.Context) {
|
||||
var repo *context.Repository
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
repo = ctx.Repo
|
||||
case *context.APIContext:
|
||||
repo = ctx.Repo
|
||||
default:
|
||||
assert.Fail(t, "context is not *context.Context or *context.APIContext")
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.Repository.RepoPath())
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := gitRepo.GetHEADBranch()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, branch)
|
||||
if branch != nil {
|
||||
ctx.Repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
|
||||
repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadUser load a user into a test context.
|
||||
func LoadUser(t *testing.T, ctx *context.Context, userID int64) {
|
||||
ctx.Doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
func LoadUser(t *testing.T, ctx gocontext.Context, userID int64) {
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
ctx.Doer = doer
|
||||
case *context.APIContext:
|
||||
ctx.Doer = doer
|
||||
default:
|
||||
assert.Fail(t, "context is not *context.Context or *context.APIContext")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LoadGitRepo load a git repo into a test context. Requires that ctx.Repo has
|
||||
|
@ -93,32 +146,6 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type mockResponseWriter struct {
|
||||
httptest.ResponseRecorder
|
||||
size int
|
||||
}
|
||||
|
||||
func (rw *mockResponseWriter) Write(b []byte) (int, error) {
|
||||
rw.size += len(b)
|
||||
return rw.ResponseRecorder.Write(b)
|
||||
}
|
||||
|
||||
func (rw *mockResponseWriter) Status() int {
|
||||
return rw.ResponseRecorder.Code
|
||||
}
|
||||
|
||||
func (rw *mockResponseWriter) Written() bool {
|
||||
return rw.ResponseRecorder.Code > 0
|
||||
}
|
||||
|
||||
func (rw *mockResponseWriter) Size() int {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockRender struct{}
|
||||
|
||||
func (tr *mockRender) TemplateLookup(tmpl string) (templates.TemplateExecutor, error) {
|
||||
|
|
|
@ -38,10 +38,12 @@ type LangType struct {
|
|||
}
|
||||
|
||||
var (
|
||||
lock *sync.RWMutex
|
||||
lock *sync.RWMutex
|
||||
|
||||
allLangs []*LangType
|
||||
allLangMap map[string]*LangType
|
||||
|
||||
matcher language.Matcher
|
||||
allLangs []*LangType
|
||||
allLangMap map[string]*LangType
|
||||
supportedTags []language.Tag
|
||||
)
|
||||
|
||||
|
@ -251,3 +253,9 @@ func (l *locale) PrettyNumber(v any) string {
|
|||
}
|
||||
return l.msgPrinter.Sprintf("%v", number.Decimal(v))
|
||||
}
|
||||
|
||||
func init() {
|
||||
// prepare a default matcher, especially for tests
|
||||
supportedTags = []language.Tag{language.English}
|
||||
matcher = language.NewMatcher(supportedTags)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"reflect"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
)
|
||||
|
||||
|
@ -25,6 +26,10 @@ var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusPro
|
|||
reflect.TypeOf(&context.PrivateContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetPrivateContext(req) },
|
||||
}
|
||||
|
||||
func RegisterHandleTypeProvider[T any](fn func(req *http.Request) ResponseStatusProvider) {
|
||||
argTypeProvider[reflect.TypeOf((*T)(nil)).Elem()] = fn
|
||||
}
|
||||
|
||||
// responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written
|
||||
type responseWriter struct {
|
||||
respWriter http.ResponseWriter
|
||||
|
@ -78,7 +83,13 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) {
|
|||
}
|
||||
}
|
||||
|
||||
func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value) []reflect.Value {
|
||||
func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value, fnInfo *routing.FuncInfo) []reflect.Value {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("unable to prepare handler arguments for %s: %v", fnInfo.String(), err)
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
isPreCheck := req == nil
|
||||
|
||||
argsIn := make([]reflect.Value, fn.Type().NumIn())
|
||||
|
@ -155,7 +166,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
|||
}
|
||||
|
||||
// prepare the arguments for the handler and do pre-check
|
||||
argsIn := prepareHandleArgsIn(resp, req, fn)
|
||||
argsIn := prepareHandleArgsIn(resp, req, fn, funcInfo)
|
||||
if req == nil {
|
||||
preCheckHandler(fn, argsIn)
|
||||
return // it's doing pre-check, just return
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue