Improve template system and panic recovery (#24461)

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>
This commit is contained in:
wxiaoguang 2023-05-04 14:36:34 +08:00 committed by GitHub
parent 75ea0d5dba
commit 5d77691d42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 277 additions and 364 deletions

View file

@ -5,7 +5,6 @@ package context
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -13,8 +12,10 @@ import (
"text/template" "text/template"
"time" "time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
) )
type routerLoggerOptions struct { type routerLoggerOptions struct {
@ -26,8 +27,6 @@ type routerLoggerOptions struct {
RequestID *string RequestID *string
} }
var signedUserNameStringPointerKey interface{} = "signedUserNameStringPointerKey"
const keyOfRequestIDInTemplate = ".RequestID" const keyOfRequestIDInTemplate = ".RequestID"
// According to: // According to:
@ -60,8 +59,6 @@ func AccessLogger() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now() start := time.Now()
identity := "-"
r := req.WithContext(context.WithValue(req.Context(), signedUserNameStringPointerKey, &identity))
var requestID string var requestID string
if needRequestID { if needRequestID {
@ -73,9 +70,14 @@ func AccessLogger() func(http.Handler) http.Handler {
reqHost = req.RemoteAddr reqHost = req.RemoteAddr
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, req)
rw := w.(ResponseWriter) 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{}) buf := bytes.NewBuffer([]byte{})
err = logTemplate.Execute(buf, routerLoggerOptions{ err = logTemplate.Execute(buf, routerLoggerOptions{
req: req, req: req,

View file

@ -222,7 +222,7 @@ func APIContexter() func(http.Handler) http.Handler {
ctx := APIContext{ ctx := APIContext{
Context: &Context{ Context: &Context{
Resp: NewResponse(w), Resp: NewResponse(w),
Data: map[string]interface{}{}, Data: middleware.GetContextData(req.Context()),
Locale: locale, Locale: locale,
Cache: cache.GetCache(), Cache: cache.GetCache(),
Repo: &Repository{ Repo: &Repository{
@ -250,17 +250,6 @@ func APIContexter() func(http.Handler) http.Handler {
ctx.Data["Context"] = &ctx ctx.Data["Context"] = &ctx
next.ServeHTTP(ctx.Resp, ctx.Req) 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
}
}
}) })
} }
} }

View file

@ -55,7 +55,7 @@ type Render interface {
type Context struct { type Context struct {
Resp ResponseWriter Resp ResponseWriter
Req *http.Request 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` PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData`
Render Render Render Render
translation.Locale translation.Locale
@ -97,7 +97,7 @@ func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
} }
// GetData returns the data // GetData returns the data
func (ctx *Context) GetData() map[string]interface{} { func (ctx *Context) GetData() middleware.ContextData {
return ctx.Data return ctx.Data
} }
@ -219,6 +219,7 @@ const tplStatus500 base.TplName = "status/500"
// HTML calls Context.HTML and renders the template to HTTP response // HTML calls Context.HTML and renders the template to HTTP response
func (ctx *Context) HTML(status int, name base.TplName) { func (ctx *Context) HTML(status int, name base.TplName) {
log.Debug("Template: %s", name) log.Debug("Template: %s", name)
tmplStartTime := time.Now() tmplStartTime := time.Now()
if !setting.IsProd { if !setting.IsProd {
ctx.Data["TemplateName"] = name ctx.Data["TemplateName"] = name
@ -226,13 +227,19 @@ func (ctx *Context) HTML(status int, name base.TplName) {
ctx.Data["TemplateLoadTimes"] = func() string { ctx.Data["TemplateLoadTimes"] = func() string {
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" 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 { err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data)
ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.") if err == nil {
return return
} }
// if rendering fails, show error page
if name != tplStatus500 {
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err)) 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
} }
} }
@ -676,7 +683,7 @@ func getCsrfOpts() CsrfOptions {
} }
// Contexter initializes a classic context for a request. // 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() rnd := templates.HTMLRenderer()
csrfOpts := getCsrfOpts() csrfOpts := getCsrfOpts()
if !setting.IsProd { if !setting.IsProd {
@ -684,34 +691,30 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
} }
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 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{ ctx := Context{
Resp: NewResponse(resp), Resp: NewResponse(resp),
Cache: mc.GetCache(), Cache: mc.GetCache(),
Locale: locale, Locale: middleware.Locale(resp, req),
Link: link, Link: setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/"),
Render: rnd, Render: rnd,
Session: session.GetSession(req), Session: session.GetSession(req),
Repo: &Repository{ Repo: &Repository{
PullRequest: &PullRequest{}, PullRequest: &PullRequest{},
}, },
Org: &Organization{}, Org: &Organization{},
Data: map[string]interface{}{ Data: middleware.GetContextData(req.Context()),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"PageStartTime": startTime,
"Link": link,
"RunModeIsProd": setting.IsProd,
},
} }
defer ctx.Close() defer ctx.Close()
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.PageData = map[string]interface{}{}
ctx.Data["PageData"] = ctx.PageData
ctx.Data["Context"] = &ctx 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]any{}
ctx.Data["PageData"] = ctx.PageData
ctx.Req = WithContext(req, &ctx) ctx.Req = WithContext(req, &ctx)
ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx) ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx)
@ -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) + `">`) 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 // 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["DisableMigrations"] = setting.Repository.DisableMigrations
ctx.Data["DisableStars"] = setting.Repository.DisableStars ctx.Data["DisableStars"] = setting.Repository.DisableStars
ctx.Data["EnableActions"] = setting.Actions.Enabled ctx.Data["EnableActions"] = setting.Actions.Enabled
@ -777,21 +770,9 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled() ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
ctx.Data["locale"] = locale
ctx.Data["AllLangs"] = translation.AllLangs() ctx.Data["AllLangs"] = translation.AllLangs()
next.ServeHTTP(ctx.Resp, ctx.Req) 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
}
}
}) })
} }
} }

View file

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
) )
// Package contains owner, access mode and optional the package descriptor // Package contains owner, access mode and optional the package descriptor
@ -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) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := Context{ ctx := Context{
Resp: NewResponse(resp), Resp: NewResponse(resp),
Data: map[string]interface{}{}, Data: middleware.GetContextData(req.Context()),
Render: rnd, Render: rnd,
} }
defer ctx.Close() defer ctx.Close()

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/web/middleware"
) )
// PrivateContext represents a context for private routes // PrivateContext represents a context for private routes
@ -62,7 +63,7 @@ func PrivateContexter() func(http.Handler) http.Handler {
ctx := &PrivateContext{ ctx := &PrivateContext{
Context: &Context{ Context: &Context{
Resp: NewResponse(w), Resp: NewResponse(w),
Data: map[string]interface{}{}, Data: middleware.GetContextData(req.Context()),
}, },
} }
defer ctx.Close() defer ctx.Close()

View file

@ -5,43 +5,12 @@ package templates
import ( import (
"strings" "strings"
"time"
"code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "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 { func AssetFS() *assetfs.LayeredFS {
return assetfs.Layered(CustomAssets(), BuiltinAssets()) return assetfs.Layered(CustomAssets(), BuiltinAssets())
} }

View file

@ -30,7 +30,7 @@ func MockContext(t *testing.T, path string) *context.Context {
resp := &mockResponseWriter{} resp := &mockResponseWriter{}
ctx := context.Context{ ctx := context.Context{
Render: &mockRender{}, Render: &mockRender{},
Data: make(map[string]interface{}), Data: make(middleware.ContextData),
Flash: &middleware.Flash{ Flash: &middleware.Flash{
Values: make(url.Values), Values: make(url.Values),
}, },

View file

@ -3,7 +3,63 @@
package middleware package middleware
// DataStore represents a data store import (
type DataStore interface { "context"
GetData() map[string]interface{} "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,
}
} }

View file

@ -18,7 +18,7 @@ var FlashNow bool
// Flash represents a one time data transfer between two requests. // Flash represents a one time data transfer between two requests.
type Flash struct { type Flash struct {
DataStore DataStore ContextDataStore
url.Values url.Values
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
} }
@ -34,7 +34,7 @@ func (f *Flash) set(name, msg string, current ...bool) {
} }
if isShow { if isShow {
f.GetData()["Flash"] = f f.DataStore.GetData()["Flash"] = f
} else { } else {
f.Set(name, msg) f.Set(name, msg)
} }

View file

@ -12,8 +12,3 @@ import (
func IsAPIPath(req *http.Request) bool { func IsAPIPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/") return strings.HasPrefix(req.URL.Path, "/api/")
} }
// IsInternalPath returns true if the specified URL is an internal API path
func IsInternalPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/internal/")
}

View file

@ -25,12 +25,12 @@ func Bind[T any](_ T) any {
} }
// SetForm set the form object // SetForm set the form object
func SetForm(data middleware.DataStore, obj interface{}) { func SetForm(data middleware.ContextDataStore, obj interface{}) {
data.GetData()["__form"] = obj data.GetData()["__form"] = obj
} }
// GetForm returns the validate form information // GetForm returns the validate form information
func GetForm(data middleware.DataStore) interface{} { func GetForm(data middleware.ContextDataStore) interface{} {
return data.GetData()["__form"] return data.GetData()["__form"]
} }

View file

@ -103,7 +103,6 @@ func CreateUser(ctx *context.APIContext) {
if err != nil { if err != nil {
log.Error(err.Error()) log.Error(err.Error())
} }
ctx.Data["Err_Password"] = true
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return return
} }
@ -201,7 +200,6 @@ func EditUser(ctx *context.APIContext) {
if err != nil { if err != nil {
log.Error(err.Error()) log.Error(err.Error())
} }
ctx.Data["Err_Password"] = true
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return return
} }

View file

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -36,7 +37,7 @@ func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecor
Req: req, Req: req,
Resp: context.NewResponse(resp), Resp: context.NewResponse(resp),
Render: rnd, Render: rnd,
Data: make(map[string]interface{}), Data: make(middleware.ContextData),
} }
defer c.Close() defer c.Close()

57
routers/common/errpage.go Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"fmt"
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
)
const tplStatus500 base.TplName = "status/500"
// RenderPanicErrorPage renders a 500 page, and it never panics
func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
combinedErr := fmt.Sprintf("%v\n%s", err, log.Stack(2))
log.Error("PANIC: %s", combinedErr)
defer func() {
if err := recover(); err != nil {
log.Error("Panic occurs again when rendering error page: %v", err)
}
}()
routing.UpdatePanicError(req.Context(), err)
httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
data := middleware.GetContextData(req.Context())
if data["locale"] == nil {
data = middleware.CommonTemplateContextData()
data["locale"] = middleware.Locale(w, req)
}
// This recovery handler could be called without Gitea's web context, so we shouldn't touch that context too much.
// Otherwise, the 500-page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic.
user, _ := data[middleware.ContextDataKeySignedUser].(*user_model.User)
if !setting.IsProd || (user != nil && user.IsAdmin) {
data["ErrorMsg"] = "PANIC: " + combinedErr
}
err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, string(tplStatus500), data)
if err != nil {
log.Error("Error occurs again when rendering error page: %v", err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker"))
}
}

View file

@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
@ -20,13 +20,26 @@ import (
chi "github.com/go-chi/chi/v5" chi "github.com/go-chi/chi/v5"
) )
// ProtocolMiddlewares returns HTTP protocol related middlewares // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
func ProtocolMiddlewares() (handlers []any) { func ProtocolMiddlewares() (handlers []any) {
// first, normalize the URL path
handlers = append(handlers, stripSlashesMiddleware)
// prepare the ContextData and panic recovery
handlers = append(handlers, func(next http.Handler) http.Handler { handlers = append(handlers, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL defer func() {
req.URL.RawPath = req.URL.EscapedPath() if err := recover(); err != nil {
RenderPanicErrorPage(resp, req, err) // it should never panic
}
}()
req = req.WithContext(middleware.WithContextData(req.Context()))
next.ServeHTTP(resp, req)
})
})
handlers = append(handlers, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
defer finished() defer finished()
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx))) next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
@ -47,9 +60,6 @@ func ProtocolMiddlewares() (handlers []any) {
handlers = append(handlers, proxy.ForwardedHeaders(opt)) handlers = append(handlers, proxy.ForwardedHeaders(opt))
} }
// Strip slashes.
handlers = append(handlers, stripSlashesMiddleware)
if !setting.Log.DisableRouterLog { if !setting.Log.DisableRouterLog {
handlers = append(handlers, routing.NewLoggerHandler()) handlers = append(handlers, routing.NewLoggerHandler())
} }
@ -58,40 +68,18 @@ func ProtocolMiddlewares() (handlers []any) {
handlers = append(handlers, context.AccessLogger()) handlers = append(handlers, context.AccessLogger())
} }
handlers = append(handlers, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// Why we need this? The Recovery() will try to render a beautiful
// error page for user, but the process can still panic again, and other
// middleware like session also may panic then we have to recover twice
// and send a simple error page that should not panic anymore.
defer func() {
if err := recover(); err != nil {
routing.UpdatePanicError(req.Context(), err)
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2))
log.Error("%v", combinedErr)
if setting.IsProd {
http.Error(resp, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} else {
http.Error(resp, combinedErr, http.StatusInternalServerError)
}
}
}()
next.ServeHTTP(resp, req)
})
})
return handlers return handlers
} }
func stripSlashesMiddleware(next http.Handler) http.Handler { func stripSlashesMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
var urlPath string // First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
req.URL.RawPath = req.URL.EscapedPath()
urlPath := req.URL.RawPath
rctx := chi.RouteContext(req.Context()) rctx := chi.RouteContext(req.Context())
if rctx != nil && rctx.RoutePath != "" { if rctx != nil && rctx.RoutePath != "" {
urlPath = rctx.RoutePath urlPath = rctx.RoutePath
} else if req.URL.RawPath != "" {
urlPath = req.URL.RawPath
} else {
urlPath = req.URL.Path
} }
sanitizedPath := &strings.Builder{} sanitizedPath := &strings.Builder{}

View file

@ -5,7 +5,6 @@
package install package install
import ( import (
goctx "context"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -53,33 +52,32 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
return dbTypeNames return dbTypeNames
} }
// Init prepare for rendering installation page // Contexter prepare for rendering installation page
func Init(ctx goctx.Context) func(next http.Handler) http.Handler { func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer() rnd := templates.HTMLRenderer()
dbTypeNames := getSupportedDbTypeNames() dbTypeNames := getSupportedDbTypeNames()
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
locale := middleware.Locale(resp, req)
startTime := time.Now()
ctx := context.Context{ ctx := context.Context{
Resp: context.NewResponse(resp), Resp: context.NewResponse(resp),
Flash: &middleware.Flash{}, Flash: &middleware.Flash{},
Locale: locale, Locale: middleware.Locale(resp, req),
Render: rnd, Render: rnd,
Data: middleware.GetContextData(req.Context()),
Session: session.GetSession(req), Session: session.GetSession(req),
Data: map[string]interface{}{
"locale": locale,
"Title": locale.Tr("install.install"),
"PageIsInstall": true,
"DbTypeNames": dbTypeNames,
"AllLangs": translation.AllLangs(),
"PageStartTime": startTime,
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
},
} }
defer ctx.Close() defer ctx.Close()
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data.MergeFrom(middleware.ContextData{
"locale": ctx.Locale,
"Title": ctx.Locale.Tr("install.install"),
"PageIsInstall": true,
"DbTypeNames": dbTypeNames,
"AllLangs": translation.AllLangs(),
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
})
ctx.Req = context.WithContext(req, &ctx) ctx.Req = context.WithContext(req, &ctx)
next.ServeHTTP(resp, ctx.Req) next.ServeHTTP(resp, ctx.Req)
}) })

View file

@ -9,76 +9,14 @@ import (
"html" "html"
"net/http" "net/http"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/healthcheck" "code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
) )
type dataStore map[string]interface{}
func (d *dataStore) GetData() map[string]interface{} {
return *d
}
func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
// Why we need this? The first recover will try to render a beautiful
// error page for user, but the process can still panic again, then
// we have to just recover twice and send a simple error page that
// should not panic anymore.
defer func() {
if err := recover(); err != nil {
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2))
log.Error("%s", combinedErr)
if setting.IsProd {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} else {
http.Error(w, combinedErr, http.StatusInternalServerError)
}
}
}()
if err := recover(); err != nil {
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2))
log.Error("%s", combinedErr)
lc := middleware.Locale(w, req)
store := dataStore{
"Language": lc.Language(),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"locale": lc,
"SignedUserID": int64(0),
"SignedUserName": "",
}
httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {
store["ErrorMsg"] = combinedErr
}
rnd := templates.HTMLRenderer()
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
if err != nil {
log.Error("%v", err)
}
}
}()
next.ServeHTTP(w, req)
})
}
}
// Routes registers the installation routes // Routes registers the installation routes
func Routes(ctx goctx.Context) *web.Route { func Routes(ctx goctx.Context) *web.Route {
base := web.NewRoute() base := web.NewRoute()
@ -86,9 +24,7 @@ func Routes(ctx goctx.Context) *web.Route {
base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/")) base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/"))
r := web.NewRoute() r := web.NewRoute()
r.Use(common.Sessioner()) r.Use(common.Sessioner(), Contexter())
r.Use(installRecovery(ctx))
r.Use(Init(ctx))
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall) r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone) r.Get("/post-install", InstallDone)

View file

@ -4,7 +4,6 @@
package web package web
import ( import (
goctx "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -13,18 +12,12 @@ import (
"path" "path"
"strings" "strings"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/services/auth"
"gitea.com/go-chi/session"
) )
func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler { func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
@ -110,62 +103,3 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
}) })
} }
} }
type dataStore map[string]interface{}
func (d *dataStore) GetData() map[string]interface{} {
return *d
}
// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so.
// This error will be created with the gitea 500 page.
func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
routing.UpdatePanicError(req.Context(), err)
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, log.Stack(2))
log.Error("%s", combinedErr)
sessionStore := session.GetSession(req)
lc := middleware.Locale(w, req)
store := dataStore{
"Language": lc.Language(),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"locale": lc,
}
// TODO: this recovery handler is usually called without Gitea's web context, so we shouldn't touch that context too much
// Otherwise, the 500 page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic
user := context.GetContextUser(req) // almost always nil
if user == nil {
// Get user from session if logged in - do not attempt to sign-in
user = auth.SessionUser(sessionStore)
}
httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd || (user != nil && user.IsAdmin) {
store["ErrorMsg"] = combinedErr
}
defer func() {
if err := recover(); err != nil {
log.Error("HTML render in Recovery handler panics again: %v", err)
}
}()
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
if err != nil {
log.Error("HTML render in Recovery handler fails again: %v", err)
}
}
}()
next.ServeHTTP(w, req)
})
}
}

View file

@ -116,38 +116,34 @@ func Routes(ctx gocontext.Context) *web.Route {
_ = templates.HTMLRenderer() _ = templates.HTMLRenderer()
common := []any{ var mid []any
common.Sessioner(),
RecoveryWith500Page(ctx),
}
if setting.EnableGzip { if setting.EnableGzip {
h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize)) h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
if err != nil { if err != nil {
log.Fatal("GzipHandlerWithOpts failed: %v", err) log.Fatal("GzipHandlerWithOpts failed: %v", err)
} }
common = append(common, h) mid = append(mid, h)
} }
if setting.Service.EnableCaptcha { if setting.Service.EnableCaptcha {
// The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
routes.RouteMethods("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...) routes.RouteMethods("/captcha/*", "GET,HEAD", append(mid, captcha.Captchaer(context.GetImageCaptcha()))...)
} }
if setting.HasRobotsTxt { if setting.HasRobotsTxt {
routes.Get("/robots.txt", append(common, misc.RobotsTxt)...) routes.Get("/robots.txt", append(mid, misc.RobotsTxt)...)
} }
// prometheus metrics endpoint - do not need to go through contexter
if setting.Metrics.Enabled { if setting.Metrics.Enabled {
prometheus.MustRegister(metrics.NewCollector()) prometheus.MustRegister(metrics.NewCollector())
routes.Get("/metrics", append(common, Metrics)...) routes.Get("/metrics", append(mid, Metrics)...)
} }
routes.Get("/ssh_info", misc.SSHInfo) routes.Get("/ssh_info", misc.SSHInfo)
routes.Get("/api/healthz", healthcheck.Check) routes.Get("/api/healthz", healthcheck.Check)
common = append(common, context.Contexter(ctx)) mid = append(mid, common.Sessioner(), context.Contexter())
group := buildAuthGroup() group := buildAuthGroup()
if err := group.Init(ctx); err != nil { if err := group.Init(ctx); err != nil {
@ -155,23 +151,23 @@ func Routes(ctx gocontext.Context) *web.Route {
} }
// Get user from session if logged in. // Get user from session if logged in.
common = append(common, auth_service.Auth(group)) mid = append(mid, auth_service.Auth(group))
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
common = append(common, middleware.GetHead) mid = append(mid, middleware.GetHead)
if setting.API.EnableSwagger { if setting.API.EnableSwagger {
// Note: The route is here but no in API routes because it renders a web page // Note: The route is here but no in API routes because it renders a web page
routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default routes.Get("/api/swagger", append(mid, misc.Swagger)...) // Render V1 by default
} }
// TODO: These really seem like things that could be folded into Contexter or as helper functions // TODO: These really seem like things that could be folded into Contexter or as helper functions
common = append(common, user.GetNotificationCount) mid = append(mid, user.GetNotificationCount)
common = append(common, repo.GetActiveStopwatch) mid = append(mid, repo.GetActiveStopwatch)
common = append(common, goGet) mid = append(mid, goGet)
others := web.NewRoute() others := web.NewRoute()
others.Use(common...) others.Use(mid...)
registerRoutes(others) registerRoutes(others)
routes.Mount("", others) routes.Mount("", others)
return routes return routes

View file

@ -13,7 +13,7 @@ import (
) )
// DataStore represents a data store // DataStore represents a data store
type DataStore middleware.DataStore type DataStore middleware.ContextDataStore
// SessionStore represents a session store // SessionStore represents a session store
type SessionStore session.Store type SessionStore session.Store

View file

@ -51,13 +51,11 @@ func authShared(ctx *context.Context, authMethod Method) error {
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
ctx.IsSigned = true ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data["SignedUser"] = ctx.Doer ctx.Data[middleware.ContextDataKeySignedUser] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["SignedUserName"] = ctx.Doer.Name
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else { } else {
ctx.Data["SignedUserID"] = int64(0) ctx.Data["SignedUserID"] = int64(0)
ctx.Data["SignedUserName"] = ""
} }
return nil return nil
} }

View file

@ -6,7 +6,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
<script> <script>
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);}); window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
window.config = { window.config = {
initCount: (window.config?.initCount ?? 0) + 1,
appUrl: '{{AppUrl}}', appUrl: '{{AppUrl}}',
appSubUrl: '{{AppSubUrl}}', appSubUrl: '{{AppSubUrl}}',
assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly

View file

@ -1,29 +1,53 @@
{{template "base/head" .}} {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics.
<div role="main" aria-label="{{.Title}}" class="page-content status-page-500"> * base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, DefaultTheme, Str2html
* locale
* ErrorMsg
* SignedUser (optional)
*/}}
<!DOCTYPE html>
<html lang="{{.locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Internal Server Error - {{AppName}}</title>
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{AssetVersion}}">
</head>
<body>
<div class="full height">
<nav class="ui secondary menu following bar light">
<div class="ui container gt-df">
<div class="item brand gt-f1">
<a href="{{AppSubUrl}}/" aria-label="{{.locale.Tr "home"}}">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{.locale.Tr "logo"}}" aria-hidden="true">
</a>
</div>
<button class="item ui icon button">{{svg "octicon-three-bars"}}</button>{{/* a fake button to make the UI looks better*/}}
</div>
</nav>
<div role="main" class="page-content status-page-500">
<p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p> <p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui container gt-mt-5"> <div class="ui container gt-mt-5">
{{if .ErrorMsg}} {{if .ErrorMsg}}
<p>{{.locale.Tr "error.occurred"}}:</p> <p>{{.locale.Tr "error.occurred"}}:</p>
<pre class="gt-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre> <pre class="gt-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre>
{{end}} {{end}}
<div class="center gt-mt-5"> <div class="center gt-mt-5">
{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}} {{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
{{if .IsAdmin}}<p>{{.locale.Tr "error.report_message" | Safe}}</p>{{end}} {{if .SignedUser.IsAdmin}}<p>{{.locale.Tr "error.report_message" | Str2html}}</p>{{end}}
</div> </div>
</div> </div>
</div> </div>
{{/* when a sub-template triggers an 500 error, its parent template has been partially rendered, </div>
then the 500 page will be rendered after that partially rendered page, the HTML/JS are totally broken.
so use this inline script to try to move it to main viewport */}} {{/* When a sub-template triggers an 500 error, its parent template has been partially rendered, then the 500 page
will be rendered after that partially rendered page, the HTML/JS are totally broken. Use this inline script to try to move it to main viewport.
And this page shouldn't include any other JS file, avoid duplicate JS execution (still due to the partial rendering).*/}}
<script type="module"> <script type="module">
const embedded = document.querySelector('.page-content .page-content.status-page-500'); const embedded = document.querySelector('.page-content .page-content.status-page-500');
if (embedded) { if (embedded) {
// move footer to main view
const footer = document.querySelector('footer');
if (footer) document.querySelector('body').append(footer);
// move the 500 error page content to main view // move the 500 error page content to main view
const embeddedParent = embedded.parentNode; const embeddedParent = embedded.parentNode;
let main = document.querySelector('.page-content'); let main = document.querySelector('.page-content');
@ -33,4 +57,5 @@ if (embedded) {
embeddedParent.remove(); // remove the unrelated 500-page elements (eg: the duplicate nav bar) embeddedParent.remove(); // remove the unrelated 500-page elements (eg: the duplicate nav bar)
} }
</script> </script>
{{template "base/footer" .}} </body>
</html>

View file

@ -5,7 +5,7 @@
<div class="ui five wide column"> <div class="ui five wide column">
<div class="ui card"> <div class="ui card">
<div id="profile-avatar" class="content gt-df"> <div id="profile-avatar" class="content gt-df">
{{if eq .SignedUserName .ContextUser.Name}} {{if eq .SignedUserID .ContextUser.ID}}
<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{.locale.Tr "user.change_avatar"}}"> <a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{.locale.Tr "user.change_avatar"}}">
{{avatar $.Context .ContextUser 290}} {{avatar $.Context .ContextUser 290}}
</a> </a>
@ -30,7 +30,7 @@
{{if .ContextUser.Location}} {{if .ContextUser.Location}}
<li>{{svg "octicon-location"}} {{.ContextUser.Location}}</li> <li>{{svg "octicon-location"}} {{.ContextUser.Location}}</li>
{{end}} {{end}}
{{if (eq .SignedUserName .ContextUser.Name)}} {{if (eq .SignedUserID .ContextUser.ID)}}
<li> <li>
{{svg "octicon-mail"}} {{svg "octicon-mail"}}
<a href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a> <a href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a>
@ -100,7 +100,7 @@
</ul> </ul>
</li> </li>
{{end}} {{end}}
{{if and .IsSigned (ne .SignedUserName .ContextUser.Name)}} {{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
<li class="follow"> <li class="follow">
{{if $.IsFollowing}} {{if $.IsFollowing}}
<form method="post" action="{{.Link}}?action=unfollow&redirect_to={{$.Link}}"> <form method="post" action="{{.Link}}?action=unfollow&redirect_to={{$.Link}}">

View file

@ -20,10 +20,6 @@ export function showGlobalErrorMessage(msg) {
* @param {ErrorEvent} e * @param {ErrorEvent} e
*/ */
function processWindowErrorEvent(e) { function processWindowErrorEvent(e) {
if (window.config.initCount > 1) {
// the page content has been loaded many times, the HTML/JS are totally broken, don't need to show error message
return;
}
if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) { if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
// At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240 // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
// If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
@ -37,13 +33,6 @@ function initGlobalErrorHandler() {
if (!window.config) { if (!window.config) {
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
} }
if (window.config.initCount > 1) {
// when a sub-templates triggers an 500 error, its parent template has been partially rendered,
// then the 500 page will be rendered after that partially rendered page, which will cause the initCount > 1
// in this case, the page is totally broken, so do not do any further error handling
console.error('initGlobalErrorHandler: Gitea global config system has already been initialized, there must be something else wrong');
return;
}
// we added an event handler for window error at the very beginning of <script> of page head // we added an event handler for window error at the very beginning of <script> of page head
// the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init // the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init
// then in this init, we can collect all error events and show them // then in this init, we can collect all error events and show them