diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /services/context/context.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | services/context/context.go | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/services/context/context.go b/services/context/context.go new file mode 100644 index 0000000..91e7b18 --- /dev/null +++ b/services/context/context.go @@ -0,0 +1,254 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "context" + "encoding/hex" + "fmt" + "html/template" + "io" + "net/http" + "net/url" + "strings" + "time" + + "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/gitrepo" + "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/web/middleware" + web_types "code.gitea.io/gitea/modules/web/types" + + "code.forgejo.org/go-chi/cache" + "code.forgejo.org/go-chi/session" +) + +// Render represents a template render +type Render interface { + TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error) + HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error +} + +// Context represents context of a request. +type Context struct { + *Base + + TemplateContext TemplateContext + + Render Render + PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` + + Cache cache.Cache + Csrf CSRFProtector + Flash *middleware.Flash + Session session.Store + + Link string // current request URL (without query string) + + 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 *Organization + Package *Package +} + +type TemplateContext map[string]any + +func init() { + web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider { + return req.Context().Value(WebContextKey).(*Context) + }) +} + +type webContextKeyType struct{} + +var WebContextKey = webContextKeyType{} + +func GetWebContext(req *http.Request) *Context { + ctx, _ := req.Context().Value(WebContextKey).(*Context) + 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(WebContextKey).(*Context); ok { + ctx = &ValidateContext{Base: ctxWeb.Base} + } else { + panic("invalid context, expect either APIContext or Context") + } + return ctx +} + +func NewTemplateContextForWeb(ctx *Context) TemplateContext { + tmplCtx := NewTemplateContext(ctx) + tmplCtx["Locale"] = ctx.Base.Locale + tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx) + return tmplCtx +} + +func NewWebContext(base *Base, render Render, session session.Store) *Context { + ctx := &Context{ + Base: base, + Render: render, + Session: session, + + Cache: mc.GetCache(), + Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), + Repo: &Repository{PullRequest: &PullRequest{}}, + Org: &Organization{}, + } + ctx.TemplateContext = NewTemplateContextForWeb(ctx) + ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} + return ctx +} + +// Contexter initializes a classic context for a request. +func Contexter() func(next http.Handler) http.Handler { + rnd := templates.HTMLRenderer() + csrfOpts := CsrfOptions{ + Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), + Cookie: setting.CSRFCookieName, + Secure: setting.SessionConfig.Secure, + CookieHTTPOnly: setting.CSRFCookieHTTPOnly, + CookieDomain: setting.SessionConfig.Domain, + CookiePath: setting.SessionConfig.CookiePath, + SameSite: setting.SessionConfig.SameSite, + } + if !setting.IsProd { + CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + base, baseCleanUp := NewBaseContext(resp, req) + defer baseCleanUp() + ctx := NewWebContext(base, rnd, session.GetSession(req)) + + ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) + ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this + ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() + ctx.Data["Link"] = ctx.Link + + // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules + ctx.PageData = map[string]any{} + ctx.Data["PageData"] = ctx.PageData + + ctx.Base.AppendContextValue(WebContextKey, ctx) + ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) + + ctx.Csrf = NewCSRFProtector(csrfOpts) + + // 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, + Values: vals, + ErrorMsg: vals.Get("error"), + SuccessMsg: vals.Get("success"), + InfoMsg: vals.Get("info"), + WarningMsg: vals.Get("warning"), + } + } + + // if there are new messages in the ctx.Flash, write them into cookie + ctx.Resp.Before(func(resp ResponseWriter) { + if val := ctx.Flash.Encode(); val != "" { + middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0) + } else if lastFlashCookie != "" { + middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, "", -1) + } + }) + + // 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") { + if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size + ctx.ServerError("ParseMultipartForm", err) + return + } + } + + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") + ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) + + ctx.Data["SystemConfig"] = setting.Config() + + // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these + ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations + ctx.Data["DisableStars"] = setting.Repository.DisableStars + ctx.Data["DisableForks"] = setting.Repository.DisableForks + ctx.Data["EnableActions"] = setting.Actions.Enabled + + ctx.Data["ManifestData"] = setting.ManifestData + + ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled() + ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() + ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled() + ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() + ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled() + + ctx.Data["AllLangs"] = translation.AllLangs() + + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +// 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 +} + +func (ctx *Context) JSONRedirect(redirect string) { + ctx.JSON(http.StatusOK, map[string]any{"redirect": redirect}) +} + +func (ctx *Context) JSONOK() { + ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it +} + +func (ctx *Context) JSONError(msg any) { + switch v := msg.(type) { + case string: + ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"}) + case template.HTML: + ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"}) + default: + panic(fmt.Sprintf("unsupported type: %T", msg)) + } +} |