mirror of https://github.com/go-gitea/gitea
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.pull/24823/head^2
parent
6ba4f89723
commit
6b33152b7d
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
Loading…
Reference in new issue