forked from gitea/gitea
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:
parent
75ea0d5dba
commit
5d77691d42
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/")
|
|
||||||
}
|
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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{}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}">
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue