Skip to content

Commit

Permalink
Improve template system and panic recovery (#24461)
Browse files Browse the repository at this point in the history
Partially for #24457

Major changes:

1. The old `signedUserNameStringPointerKey` is quite hacky, use
`ctx.Data[SignedUser]` instead
2. Move duplicate code from `Contexter` to `CommonTemplateContextData`
3. Remove incorrect copying&pasting code `ctx.Data["Err_Password"] =
true` in API handlers
4. Use one unique `RenderPanicErrorPage` for panic error page rendering
5. Move `stripSlashesMiddleware` to be the first middleware
6. Install global panic recovery handler, it works for both `install`
and `web`
7. Make `500.tmpl` only depend minimal template functions/variables,
avoid triggering new panics

Screenshot:

<details>

![image](https://user-images.githubusercontent.com/2114189/235444895-cecbabb8-e7dc-4360-a31c-b982d11946a7.png)

</details>
  • Loading branch information
wxiaoguang authored May 4, 2023
1 parent 75ea0d5 commit 5d77691
Show file tree
Hide file tree
Showing 25 changed files with 276 additions and 363 deletions.
14 changes: 8 additions & 6 deletions modules/context/access_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ package context

import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"strings"
"text/template"
"time"

user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
)

type routerLoggerOptions struct {
Expand All @@ -26,8 +27,6 @@ type routerLoggerOptions struct {
RequestID *string
}

var signedUserNameStringPointerKey interface{} = "signedUserNameStringPointerKey"

const keyOfRequestIDInTemplate = ".RequestID"

// According to:
Expand Down Expand Up @@ -60,8 +59,6 @@ func AccessLogger() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
identity := "-"
r := req.WithContext(context.WithValue(req.Context(), signedUserNameStringPointerKey, &identity))

var requestID string
if needRequestID {
Expand All @@ -73,9 +70,14 @@ func AccessLogger() func(http.Handler) http.Handler {
reqHost = req.RemoteAddr
}

next.ServeHTTP(w, r)
next.ServeHTTP(w, req)
rw := w.(ResponseWriter)

identity := "-"
data := middleware.GetContextData(req.Context())
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
identity = signedUser.Name
}
buf := bytes.NewBuffer([]byte{})
err = logTemplate.Execute(buf, routerLoggerOptions{
req: req,
Expand Down
13 changes: 1 addition & 12 deletions modules/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func APIContexter() func(http.Handler) http.Handler {
ctx := APIContext{
Context: &Context{
Resp: NewResponse(w),
Data: map[string]interface{}{},
Data: middleware.GetContextData(req.Context()),
Locale: locale,
Cache: cache.GetCache(),
Repo: &Repository{
Expand Down Expand Up @@ -250,17 +250,6 @@ func APIContexter() func(http.Handler) http.Handler {
ctx.Data["Context"] = &ctx

next.ServeHTTP(ctx.Resp, ctx.Req)

// Handle adding signedUserName to the context for the AccessLogger
usernameInterface := ctx.Data["SignedUserName"]
identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey)
if usernameInterface != nil && identityPtrInterface != nil {
username := usernameInterface.(string)
identityPtr := identityPtrInterface.(*string)
if identityPtr != nil && username != "" {
*identityPtr = username
}
}
})
}
}
Expand Down
73 changes: 27 additions & 46 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type Render interface {
type Context struct {
Resp ResponseWriter
Req *http.Request
Data map[string]interface{} // data used by MVC templates
Data middleware.ContextData // data used by MVC templates
PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData`
Render Render
translation.Locale
Expand Down Expand Up @@ -97,7 +97,7 @@ func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
}

// GetData returns the data
func (ctx *Context) GetData() map[string]interface{} {
func (ctx *Context) GetData() middleware.ContextData {
return ctx.Data
}

Expand Down Expand Up @@ -219,20 +219,27 @@ const tplStatus500 base.TplName = "status/500"
// HTML calls Context.HTML and renders the template to HTTP response
func (ctx *Context) HTML(status int, name base.TplName) {
log.Debug("Template: %s", name)

tmplStartTime := time.Now()
if !setting.IsProd {
ctx.Data["TemplateName"] = name
}
ctx.Data["TemplateLoadTimes"] = func() string {
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
}
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
if status == http.StatusInternalServerError && name == tplStatus500 {
ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.")
return
}

err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data)
if err == nil {
return
}

// if rendering fails, show error page
if name != tplStatus500 {
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
ctx.ServerError("Render failed", err)
ctx.ServerError("Render failed", err) // show the 500 error page
} else {
ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
return
}
}

Expand Down Expand Up @@ -676,42 +683,38 @@ func getCsrfOpts() CsrfOptions {
}

// Contexter initializes a classic context for a request.
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
csrfOpts := getCsrfOpts()
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) {
locale := middleware.Locale(resp, req)
startTime := time.Now()
link := setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/")

ctx := Context{
Resp: NewResponse(resp),
Cache: mc.GetCache(),
Locale: locale,
Link: link,
Locale: middleware.Locale(resp, req),
Link: setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/"),
Render: rnd,
Session: session.GetSession(req),
Repo: &Repository{
PullRequest: &PullRequest{},
},
Org: &Organization{},
Data: map[string]interface{}{
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"PageStartTime": startTime,
"Link": link,
"RunModeIsProd": setting.IsProd,
},
Org: &Organization{},
Data: middleware.GetContextData(req.Context()),
}
defer ctx.Close()

ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data["Context"] = &ctx
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["Link"] = ctx.Link
ctx.Data["locale"] = ctx.Locale

// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
ctx.PageData = map[string]interface{}{}
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
ctx.Data["Context"] = &ctx

ctx.Req = WithContext(req, &ctx)
ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx)
Expand Down Expand Up @@ -755,16 +758,6 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)

// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
ctx.Data["IsLandingPageHome"] = setting.LandingPageURL == setting.LandingPageHome
ctx.Data["IsLandingPageExplore"] = setting.LandingPageURL == setting.LandingPageExplore
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations

ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage
ctx.Data["ShowFooterVersion"] = setting.Other.ShowFooterVersion

ctx.Data["EnableSwagger"] = setting.API.EnableSwagger
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
ctx.Data["DisableStars"] = setting.Repository.DisableStars
ctx.Data["EnableActions"] = setting.Actions.Enabled
Expand All @@ -777,21 +770,9 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()

ctx.Data["locale"] = locale
ctx.Data["AllLangs"] = translation.AllLangs()

next.ServeHTTP(ctx.Resp, ctx.Req)

// Handle adding signedUserName to the context for the AccessLogger
usernameInterface := ctx.Data["SignedUserName"]
identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey)
if usernameInterface != nil && identityPtrInterface != nil {
username := usernameInterface.(string)
identityPtr := identityPtrInterface.(*string)
if identityPtr != nil && username != "" {
*identityPtr = username
}
}
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion modules/context/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
)

// Package contains owner, access mode and optional the package descriptor
Expand Down Expand Up @@ -136,7 +137,7 @@ func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handle
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := Context{
Resp: NewResponse(resp),
Data: map[string]interface{}{},
Data: middleware.GetContextData(req.Context()),
Render: rnd,
}
defer ctx.Close()
Expand Down
3 changes: 2 additions & 1 deletion modules/context/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/web/middleware"
)

// PrivateContext represents a context for private routes
Expand Down Expand Up @@ -62,7 +63,7 @@ func PrivateContexter() func(http.Handler) http.Handler {
ctx := &PrivateContext{
Context: &Context{
Resp: NewResponse(w),
Data: map[string]interface{}{},
Data: middleware.GetContextData(req.Context()),
},
}
defer ctx.Close()
Expand Down
31 changes: 0 additions & 31 deletions modules/templates/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,12 @@ package templates

import (
"strings"
"time"

"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)

// Vars represents variables to be render in golang templates
type Vars map[string]interface{}

// Merge merges another vars to the current, another Vars will override the current
func (vars Vars) Merge(another map[string]interface{}) Vars {
for k, v := range another {
vars[k] = v
}
return vars
}

// BaseVars returns all basic vars
func BaseVars() Vars {
startTime := time.Now()
return map[string]interface{}{
"IsLandingPageHome": setting.LandingPageURL == setting.LandingPageHome,
"IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore,
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,

"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
"ShowFooterVersion": setting.Other.ShowFooterVersion,
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,

"EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
"PageStartTime": startTime,
}
}

func AssetFS() *assetfs.LayeredFS {
return assetfs.Layered(CustomAssets(), BuiltinAssets())
}
Expand Down
2 changes: 1 addition & 1 deletion modules/test/context_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func MockContext(t *testing.T, path string) *context.Context {
resp := &mockResponseWriter{}
ctx := context.Context{
Render: &mockRender{},
Data: make(map[string]interface{}),
Data: make(middleware.ContextData),
Flash: &middleware.Flash{
Values: make(url.Values),
},
Expand Down
62 changes: 59 additions & 3 deletions modules/web/middleware/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,63 @@

package middleware

// DataStore represents a data store
type DataStore interface {
GetData() map[string]interface{}
import (
"context"
"time"

"code.gitea.io/gitea/modules/setting"
)

// ContextDataStore represents a data store
type ContextDataStore interface {
GetData() ContextData
}

type ContextData map[string]any

func (ds ContextData) GetData() map[string]any {
return ds
}

func (ds ContextData) MergeFrom(other ContextData) ContextData {
for k, v := range other {
ds[k] = v
}
return ds
}

const ContextDataKeySignedUser = "SignedUser"

type contextDataKeyType struct{}

var contextDataKey contextDataKeyType

func WithContextData(c context.Context) context.Context {
return context.WithValue(c, contextDataKey, make(ContextData, 10))
}

func GetContextData(c context.Context) ContextData {
if ds, ok := c.Value(contextDataKey).(ContextData); ok {
return ds
}
return nil
}

func CommonTemplateContextData() ContextData {
return ContextData{
"IsLandingPageHome": setting.LandingPageURL == setting.LandingPageHome,
"IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore,
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,

"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
"ShowFooterVersion": setting.Other.ShowFooterVersion,
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,

"EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
"PageStartTime": time.Now(),

"RunModeIsProd": setting.IsProd,
}
}
4 changes: 2 additions & 2 deletions modules/web/middleware/flash.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var FlashNow bool

// Flash represents a one time data transfer between two requests.
type Flash struct {
DataStore
DataStore ContextDataStore
url.Values
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
}
Expand All @@ -34,7 +34,7 @@ func (f *Flash) set(name, msg string, current ...bool) {
}

if isShow {
f.GetData()["Flash"] = f
f.DataStore.GetData()["Flash"] = f
} else {
f.Set(name, msg)
}
Expand Down
Loading

0 comments on commit 5d77691

Please sign in to comment.