code.gitea.io/gitea@v1.22.3/services/context/context.go (about)

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