mirror of
https://github.com/go-gitea/gitea
synced 2025-01-08 21:26:00 +01:00
2a828e2798
In history (from some legacy frameworks), both `:name` and `name` are supported as path path name, `:name` is an alias to `name`. To make code consistent, now we should only use `name` but not `:name`. Also added panic check in related functions to make sure the name won't be abused in case some downstreams still use them.
199 lines
6.2 KiB
Go
199 lines
6.2 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/httplib"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/reqctx"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/translation"
|
|
"code.gitea.io/gitea/modules/web/middleware"
|
|
)
|
|
|
|
type BaseContextKeyType struct{}
|
|
|
|
var BaseContextKey BaseContextKeyType
|
|
|
|
type Base struct {
|
|
context.Context
|
|
reqctx.RequestDataStore
|
|
|
|
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 reqctx.ContextData
|
|
|
|
// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
|
|
Locale translation.Locale
|
|
}
|
|
|
|
// 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.WrittenStatus() != 0
|
|
}
|
|
|
|
func (b *Base) WrittenStatus() int {
|
|
return b.Resp.WrittenStatus()
|
|
}
|
|
|
|
// 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 any) {
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
_, _ = b.Resp.Write(bs)
|
|
}
|
|
|
|
// 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 !httplib.IsRelativeURL(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)
|
|
}
|
|
// in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
|
|
if b.Req.Header.Get("HX-Request") == "true" {
|
|
b.Resp.Header().Set("HX-Redirect", location)
|
|
// we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
|
|
// so as to give htmx redirect logic a chance to run
|
|
b.Status(http.StatusNoContent)
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
|
|
func (b *Base) Tr(msg string, args ...any) template.HTML {
|
|
return b.Locale.Tr(msg, args...)
|
|
}
|
|
|
|
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
|
return b.Locale.TrN(cnt, key1, keyN, args...)
|
|
}
|
|
|
|
func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
|
|
ds := reqctx.GetRequestDataStore(req.Context())
|
|
b := &Base{
|
|
Context: req.Context(),
|
|
RequestDataStore: ds,
|
|
Req: req,
|
|
Resp: WrapResponseWriter(resp),
|
|
Locale: middleware.Locale(resp, req),
|
|
Data: ds.GetData(),
|
|
}
|
|
b.Req = b.Req.WithContext(b)
|
|
ds.SetContextValue(BaseContextKey, b)
|
|
ds.SetContextValue(translation.ContextKey, b.Locale)
|
|
ds.SetContextValue(httplib.RequestContextKey, b.Req)
|
|
return b
|
|
}
|
|
|
|
func NewBaseContextForTest(resp http.ResponseWriter, req *http.Request) *Base {
|
|
if !setting.IsInTesting {
|
|
panic("This function is only for testing")
|
|
}
|
|
ctx := reqctx.NewRequestContextForTest(req.Context())
|
|
*req = *req.WithContext(ctx)
|
|
return NewBaseContext(resp, req)
|
|
}
|