Files
Gogs/internal/context/context.go
2026-01-25 12:05:56 +00:00

434 lines
12 KiB
Go

package context
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/flamego/cache"
"github.com/flamego/csrf"
"github.com/flamego/flamego"
"github.com/flamego/i18n"
"github.com/flamego/session"
"github.com/flamego/template"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/form"
"gogs.io/gogs/internal/lazyregexp"
gogstemplate "gogs.io/gogs/internal/template"
)
// Resp is a wrapper for ResponseWriter to provide compatibility.
type Resp struct {
http.ResponseWriter
}
// Write writes data to the response.
func (r *Resp) Write(data []byte) (int, error) {
return r.ResponseWriter.Write(data)
}
// Req is a wrapper for http.Request to provide compatibility.
type Req struct {
*http.Request
}
// Context represents context of a request.
type Context struct {
flamego.Context
template.Template
i18n.Locale
Cache cache.Cache
csrf csrf.CSRF
Flash *FlashData
Session session.Session
Resp *Resp
Req *Req
ResponseWriter http.ResponseWriter
Request *http.Request
Data template.Data
Link string // Current request URL
User *database.User
IsLogged bool
IsBasicAuth bool
IsTokenAuth bool
Repo *Repository
Org *Organization
}
// FlashData represents flash data structure.
type FlashData struct {
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
}
// Error sets error message.
func (f *FlashData) Error(msg string) {
f.ErrorMsg = msg
}
// Success sets success message.
func (f *FlashData) Success(msg string) {
f.SuccessMsg = msg
}
// Info sets info message.
func (f *FlashData) Info(msg string) {
f.InfoMsg = msg
}
// Warning sets warning message.
func (f *FlashData) Warning(msg string) {
f.WarningMsg = msg
}
// RawTitle sets the "Title" field in template data.
func (c *Context) RawTitle(title string) {
c.Data["Title"] = title
}
// Tr is a wrapper for i18n.Locale.Translate.
func (c *Context) Tr(key string, args ...any) string {
return c.Locale.Translate(key, args...)
}
// Title localizes the "Title" field in template data.
func (c *Context) Title(locale string) {
c.RawTitle(c.Tr(locale))
}
// PageIs sets "PageIsxxx" field in template data.
func (c *Context) PageIs(name string) {
c.Data["PageIs"+name] = true
}
// Require sets "Requirexxx" field in template data.
func (c *Context) Require(name string) {
c.Data["Require"+name] = true
}
func (c *Context) RequireHighlightJS() {
c.Require("HighlightJS")
}
func (c *Context) RequireSimpleMDE() {
c.Require("SimpleMDE")
}
func (c *Context) RequireAutosize() {
c.Require("Autosize")
}
func (c *Context) RequireDropzone() {
c.Require("Dropzone")
}
// FormErr sets "Err_xxx" field in template data.
func (c *Context) FormErr(names ...string) {
for i := range names {
c.Data["Err_"+names[i]] = true
}
}
// UserID returns ID of current logged in user.
// It returns 0 if visitor is anonymous.
func (c *Context) UserID() int64 {
if !c.IsLogged {
return 0
}
return c.User.ID
}
func (c *Context) GetErrMsg() string {
return c.Data["ErrorMsg"].(string)
}
// HasError returns true if error occurs in form validation.
func (c *Context) HasError() bool {
hasErr, ok := c.Data["HasError"]
if !ok {
return false
}
c.Flash.ErrorMsg = c.Data["ErrorMsg"].(string)
c.Data["Flash"] = c.Flash
return hasErr.(bool)
}
// HasValue returns true if value of given name exists.
func (c *Context) HasValue(name string) bool {
_, ok := c.Data[name]
return ok
}
// Status sets the HTTP status code.
func (c *Context) Status(status int) {
c.ResponseWriter.WriteHeader(status)
}
// JSON renders JSON response with given status and data.
func (c *Context) JSON(status int, data any) {
c.ResponseWriter.Header().Set("Content-Type", "application/json")
c.ResponseWriter.WriteHeader(status)
json.NewEncoder(c.ResponseWriter).Encode(data)
}
// Header returns the response header map.
func (c *Context) Header() http.Header {
return c.ResponseWriter.Header()
}
// Written returns whether the response has been written.
func (c *Context) Written() bool {
// In Flamego, we need to track this ourselves or check the response writer
// For now, we'll assume if status code is set, it's written
// This is a simplification - in production, you'd want a proper wrapper
return false // TODO: Implement proper tracking
}
// Write writes data to the response.
func (c *Context) Write(data []byte) (int, error) {
return c.ResponseWriter.Write(data)
}
// ParamsInt64 returns value of the given bind parameter parsed as int64.
func (c *Context) ParamsInt64(name string) int64 {
return c.Context.ParamInt64(name)
}
// Language returns the language tag from the current locale.
func (c *Context) Language() string {
// Flamego's i18n.Locale doesn't have a Language() method
// We need to use a different approach or store the language
// For now, return empty string as a placeholder
return "" // TODO: Implement proper language tracking
}
// SetCookie sets a cookie.
func (c *Context) SetCookie(name, value string, maxAge int, path string) {
http.SetCookie(c.ResponseWriter, &http.Cookie{
Name: name,
Value: value,
MaxAge: maxAge,
Path: path,
HttpOnly: true,
})
}
// GetCookie gets a cookie value.
func (c *Context) GetCookie(name string) string {
cookie, err := c.Request.Cookie(name)
if err != nil {
return ""
}
return cookie.Value
}
// HTML responses template with given status.
func (c *Context) HTML(status int, name string) {
log.Trace("Template: %s", name)
c.ResponseWriter.WriteHeader(status)
c.Template.HTML(status, name)
}
// Success responses template with status http.StatusOK.
func (c *Context) Success(name string) {
c.HTML(http.StatusOK, name)
}
// JSONSuccess responses JSON with status http.StatusOK.
func (c *Context) JSONSuccess(data any) {
c.JSON(http.StatusOK, data)
}
// RawRedirect simply calls underlying Redirect method with no escape.
func (c *Context) RawRedirect(location string, status ...int) {
code := http.StatusFound
if len(status) > 0 {
code = status[0]
}
http.Redirect(c.ResponseWriter, c.Request, location, code)
}
// Redirect responses redirection with given location and status.
// It escapes special characters in the location string.
func (c *Context) Redirect(location string, status ...int) {
c.RawRedirect(gogstemplate.EscapePound(location), status...)
}
// RedirectSubpath responses redirection with given location and status.
// It prepends setting.Server.Subpath to the location string.
func (c *Context) RedirectSubpath(location string, status ...int) {
c.Redirect(conf.Server.Subpath+location, status...)
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (c *Context) RenderWithErr(msg, tpl string, f any) {
if f != nil {
form.Assign(f, c.Data)
}
c.Flash.ErrorMsg = msg
c.Data["Flash"] = c.Flash
c.HTML(http.StatusOK, tpl)
}
// NotFound renders the 404 page.
func (c *Context) NotFound() {
c.Title("status.page_not_found")
c.HTML(http.StatusNotFound, fmt.Sprintf("status/%d", http.StatusNotFound))
}
// Error renders the 500 page.
func (c *Context) Error(err error, msg string) {
log.ErrorDepth(4, "%s: %v", msg, err)
c.Title("status.internal_server_error")
// Only in non-production mode or admin can see the actual error message.
if !conf.IsProdMode() || (c.IsLogged && c.User.IsAdmin) {
c.Data["ErrorMsg"] = err
}
c.HTML(http.StatusInternalServerError, fmt.Sprintf("status/%d", http.StatusInternalServerError))
}
// Errorf renders the 500 response with formatted message.
func (c *Context) Errorf(err error, format string, args ...any) {
c.Error(err, fmt.Sprintf(format, args...))
}
// NotFoundOrError responses with 404 page for not found error and 500 page otherwise.
func (c *Context) NotFoundOrError(err error, msg string) {
if errutil.IsNotFound(err) {
c.NotFound()
return
}
c.Error(err, msg)
}
// NotFoundOrErrorf is same as NotFoundOrError but with formatted message.
func (c *Context) NotFoundOrErrorf(err error, format string, args ...any) {
c.NotFoundOrError(err, fmt.Sprintf(format, args...))
}
func (c *Context) PlainText(status int, msg string) {
c.ResponseWriter.Header().Set("Content-Type", "text/plain; charset=utf-8")
c.ResponseWriter.WriteHeader(status)
c.ResponseWriter.Write([]byte(msg))
}
func (c *Context) ServeContent(name string, r io.ReadSeeker, params ...any) {
modtime := time.Now()
for _, p := range params {
switch v := p.(type) {
case time.Time:
modtime = v
}
}
c.ResponseWriter.Header().Set("Content-Description", "File Transfer")
c.ResponseWriter.Header().Set("Content-Type", "application/octet-stream")
c.ResponseWriter.Header().Set("Content-Disposition", "attachment; filename="+name)
c.ResponseWriter.Header().Set("Content-Transfer-Encoding", "binary")
c.ResponseWriter.Header().Set("Expires", "0")
c.ResponseWriter.Header().Set("Cache-Control", "must-revalidate")
c.ResponseWriter.Header().Set("Pragma", "public")
http.ServeContent(c.ResponseWriter, c.Request, name, modtime, r)
}
// csrfTokenExcludePattern matches characters that are not used for generating
// CSRF tokens, see all possible characters at
// https://github.com/go-macaron/csrf/blob/5d38f39de352972063d1ef026fc477283841bb9b/csrf.go#L148.
var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`)
// Contexter initializes a classic context for a request.
func Contexter(store Store) flamego.Handler {
return func(fctx flamego.Context, tpl template.Template, l i18n.Locale, cache cache.Cache, sess session.Session, x csrf.CSRF, w http.ResponseWriter, req *http.Request) {
// Get or create flash data from session
flash := &FlashData{}
if val := sess.Get("flamego::session::flash"); val != nil {
if f, ok := val.(*FlashData); ok {
flash = f
}
}
c := &Context{
Context: fctx,
Template: tpl,
Locale: l,
Cache: cache,
csrf: x,
Flash: flash,
Session: sess,
Resp: &Resp{w},
Req: &Req{req},
ResponseWriter: w,
Request: req,
Data: make(template.Data),
Link: conf.Server.Subpath + strings.TrimSuffix(req.URL.Path, "/"),
Repo: &Repository{
PullRequest: &PullRequest{},
},
Org: &Organization{},
}
c.Data["Link"] = gogstemplate.EscapePound(c.Link)
c.Data["PageStartTime"] = time.Now()
if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
w.Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "3600")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With")
}
// Get user from session or header when possible
c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(store, fctx, sess)
if c.User != nil {
c.IsLogged = true
c.Data["IsLogged"] = c.IsLogged
c.Data["LoggedUser"] = c.User
c.Data["LoggedUserID"] = c.User.ID
c.Data["LoggedUserName"] = c.User.Name
c.Data["IsAdmin"] = c.User.IsAdmin
} else {
c.Data["LoggedUserID"] = 0
c.Data["LoggedUserName"] = ""
}
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
if req.Method == "POST" && strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") {
if err := req.ParseMultipartForm(conf.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
c.Error(err, "parse multipart form")
return
}
}
// 🚨 SECURITY: Prevent XSS from injected CSRF cookie by stripping all
// characters that are not used for generating CSRF tokens, see
// https://github.com/gogs/gogs/issues/6953 for details.
csrfToken := csrfTokenExcludePattern.ReplaceAllString(x.Token(), "")
c.Data["CSRFToken"] = csrfToken
c.Data["CSRFTokenHTML"] = gogstemplate.Safe(`<input type="hidden" name="_csrf" value="` + csrfToken + `">`)
log.Trace("Session ID: %s", sess.ID())
log.Trace("CSRF Token: %v", c.Data["CSRFToken"])
c.Data["ShowRegistrationButton"] = !conf.Auth.DisableRegistration
c.Data["ShowFooterBranding"] = conf.Other.ShowFooterBranding
c.renderNoticeBanner()
// 🚨 SECURITY: Prevent MIME type sniffing in some browsers,
// see https://github.com/gogs/gogs/issues/5397 for details.
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
fctx.MapTo(c, (*Context)(nil))
}
}