1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2020 The Gitea Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package context
6
7import (
8 "context"
9 "encoding/hex"
10 "fmt"
11 "html/template"
12 "io"
13 "net/http"
14 "net/url"
15 "strings"
16 "time"
17
18 "forgejo.org/models/unit"
19 user_model "forgejo.org/models/user"
20 mc "forgejo.org/modules/cache"
21 "forgejo.org/modules/gitrepo"
22 "forgejo.org/modules/httpcache"
23 "forgejo.org/modules/setting"
24 "forgejo.org/modules/templates"
25 "forgejo.org/modules/translation"
26 "forgejo.org/modules/web"
27 "forgejo.org/modules/web/middleware"
28 web_types "forgejo.org/modules/web/types"
29
30 "code.forgejo.org/go-chi/cache"
31 "code.forgejo.org/go-chi/session"
32)
33
34// Render represents a template render
35type Render interface {
36 TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
37 HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
38}
39
40// Context represents context of a request.
41type Context struct {
42 *Base
43
44 TemplateContext TemplateContext
45
46 Render Render
47 PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
48
49 Cache cache.Cache
50 Csrf CSRFProtector
51 Flash *middleware.Flash
52 Session session.Store
53
54 Link string // current request URL (without query string)
55
56 Doer *user_model.User // current signed-in user
57 IsSigned bool
58 IsBasicAuth bool
59
60 ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
61
62 Repo *Repository
63 Org *Organization
64 Package *Package
65}
66
67type TemplateContext map[string]any
68
69func init() {
70 web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
71 return req.Context().Value(WebContextKey).(*Context)
72 })
73}
74
75type webContextKeyType struct{}
76
77var WebContextKey = webContextKeyType{}
78
79func GetWebContext(req *http.Request) *Context {
80 ctx, _ := req.Context().Value(WebContextKey).(*Context)
81 return ctx
82}
83
84// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
85type ValidateContext struct {
86 *Base
87}
88
89// GetValidateContext gets a context for middleware form validation
90func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
91 if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
92 ctx = &ValidateContext{Base: ctxAPI.Base}
93 } else if ctxWeb, ok := req.Context().Value(WebContextKey).(*Context); ok {
94 ctx = &ValidateContext{Base: ctxWeb.Base}
95 } else {
96 panic("invalid context, expect either APIContext or Context")
97 }
98 return ctx
99}
100
101func NewTemplateContextForWeb(ctx *Context) TemplateContext {
102 tmplCtx := NewTemplateContext(ctx)
103 tmplCtx["Locale"] = ctx.Locale
104 tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
105 return tmplCtx
106}
107
108func NewWebContext(base *Base, render Render, session session.Store) *Context {
109 ctx := &Context{
110 Base: base,
111 Render: render,
112 Session: session,
113
114 Cache: mc.GetCache(),
115 Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
116 Repo: &Repository{PullRequest: &PullRequest{}},
117 Org: &Organization{},
118 }
119 ctx.TemplateContext = NewTemplateContextForWeb(ctx)
120 ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
121 return ctx
122}
123
124// Contexter initializes a classic context for a request.
125func Contexter() func(next http.Handler) http.Handler {
126 rnd := templates.HTMLRenderer()
127 csrfOpts := CsrfOptions{
128 Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
129 Cookie: setting.CSRFCookieName,
130 Secure: setting.SessionConfig.Secure,
131 CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
132 CookieDomain: setting.SessionConfig.Domain,
133 CookiePath: setting.SessionConfig.CookiePath,
134 SameSite: setting.SessionConfig.SameSite,
135 }
136 if !setting.IsProd {
137 CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
138 }
139 return func(next http.Handler) http.Handler {
140 return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
141 base, baseCleanUp := NewBaseContext(resp, req)
142 defer baseCleanUp()
143 ctx := NewWebContext(base, rnd, session.GetSession(req))
144
145 ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
146 ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
147 ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
148 ctx.Data["Link"] = ctx.Link
149
150 // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
151 ctx.PageData = map[string]any{}
152 ctx.Data["PageData"] = ctx.PageData
153
154 ctx.AppendContextValue(WebContextKey, ctx)
155 ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
156
157 ctx.Csrf = NewCSRFProtector(csrfOpts)
158
159 // Get the last flash message from cookie
160 lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
161 if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
162 // store last Flash message into the template data, to render it
163 ctx.Data["Flash"] = &middleware.Flash{
164 DataStore: ctx,
165 Values: vals,
166 ErrorMsg: vals.Get("error"),
167 SuccessMsg: vals.Get("success"),
168 InfoMsg: vals.Get("info"),
169 WarningMsg: vals.Get("warning"),
170 }
171 }
172
173 // if there are new messages in the ctx.Flash, write them into cookie
174 ctx.Resp.Before(func(resp ResponseWriter) {
175 if val := ctx.Flash.Encode(); val != "" {
176 middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0)
177 } else if lastFlashCookie != "" {
178 middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, "", -1)
179 }
180 })
181
182 // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
183 if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
184 if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
185 ctx.ServerError("ParseMultipartForm", err)
186 return
187 }
188 }
189
190 httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
191 ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
192
193 ctx.Data["SystemConfig"] = setting.Config()
194
195 // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
196 ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
197 ctx.Data["DisableStars"] = setting.Repository.DisableStars
198 ctx.Data["DisableForks"] = setting.Repository.DisableForks
199 ctx.Data["EnableActions"] = setting.Actions.Enabled
200
201 ctx.Data["ManifestData"] = setting.ManifestData
202
203 ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
204 ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
205 ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled()
206 ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
207 ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
208
209 ctx.Data["AllLangs"] = translation.AllLangs()
210
211 next.ServeHTTP(ctx.Resp, ctx.Req)
212 })
213 }
214}
215
216// HasError returns true if error occurs in form validation.
217// Attention: this function changes ctx.Data and ctx.Flash
218func (ctx *Context) HasError() bool {
219 hasErr, ok := ctx.Data["HasError"]
220 if !ok {
221 return false
222 }
223 ctx.Flash.ErrorMsg = ctx.GetErrMsg()
224 ctx.Data["Flash"] = ctx.Flash
225 return hasErr.(bool)
226}
227
228// GetErrMsg returns error message in form validation.
229func (ctx *Context) GetErrMsg() string {
230 msg, _ := ctx.Data["ErrorMsg"].(string)
231 if msg == "" {
232 msg = "invalid form data"
233 }
234 return msg
235}
236
237func (ctx *Context) JSONRedirect(redirect string) {
238 ctx.JSON(http.StatusOK, map[string]any{"redirect": redirect})
239}
240
241func (ctx *Context) JSONOK() {
242 ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
243}
244
245func (ctx *Context) JSONError(msg any) {
246 switch v := msg.(type) {
247 case string:
248 ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
249 case template.HTML:
250 ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
251 default:
252 panic(fmt.Sprintf("unsupported type: %T", msg))
253 }
254}