Before: * `{{.locale.Tr ...}}` * `{{$.locale.Tr ...}}` * `{{$.root.locale.Tr ...}}` * `{{template "sub" .}}` * `{{template "sub" (dict "locale" $.locale)}}` * `{{template "sub" (dict "root" $)}}` * ..... With context function: only need to `{{ctx.Locale.Tr ...}}` The "ctx" could be considered as a super-global variable for all templates including sub-templates. To avoid potential risks (any bug in the template context function package), this PR only starts using "ctx" in "head.tmpl" and "footer.tmpl" and it has a "DataRaceCheck". If there is anything wrong, the code can be fixed or reverted easily.tags/v1.21.0-rc0
@@ -5,6 +5,7 @@ | |||
package context | |||
import ( | |||
"context" | |||
"html" | |||
"html/template" | |||
"io" | |||
@@ -31,14 +32,16 @@ import ( | |||
// Render represents a template render | |||
type Render interface { | |||
TemplateLookup(tmpl string) (templates.TemplateExecutor, error) | |||
HTML(w io.Writer, status int, name string, data any) error | |||
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error) | |||
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error | |||
} | |||
// Context represents context of a request. | |||
type Context struct { | |||
*Base | |||
TemplateContext TemplateContext | |||
Render Render | |||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` | |||
@@ -60,6 +63,8 @@ type Context struct { | |||
Package *Package | |||
} | |||
type TemplateContext map[string]any | |||
func init() { | |||
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider { | |||
return req.Context().Value(WebContextKey).(*Context) | |||
@@ -133,8 +138,12 @@ func Contexter() func(next http.Handler) http.Handler { | |||
} | |||
defer baseCleanUp() | |||
// TODO: "install.go" also shares the same logic, which should be refactored to a general function | |||
ctx.TemplateContext = NewTemplateContext(ctx) | |||
ctx.TemplateContext["Locale"] = ctx.Locale | |||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) | |||
ctx.Data["Context"] = &ctx | |||
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this | |||
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() | |||
ctx.Data["Link"] = ctx.Link | |||
ctx.Data["locale"] = ctx.Locale |
@@ -75,7 +75,7 @@ func (ctx *Context) HTML(status int, name base.TplName) { | |||
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" | |||
} | |||
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data) | |||
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext) | |||
if err == nil { | |||
return | |||
} | |||
@@ -93,7 +93,7 @@ func (ctx *Context) HTML(status int, name base.TplName) { | |||
// RenderToString renders the template content to a string | |||
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { | |||
var buf strings.Builder | |||
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data) | |||
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext) | |||
return buf.String(), err | |||
} | |||
@@ -0,0 +1,49 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package context | |||
import ( | |||
"context" | |||
"errors" | |||
"time" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
var _ context.Context = TemplateContext(nil) | |||
func NewTemplateContext(ctx context.Context) TemplateContext { | |||
return TemplateContext{"_ctx": ctx} | |||
} | |||
func (c TemplateContext) parentContext() context.Context { | |||
return c["_ctx"].(context.Context) | |||
} | |||
func (c TemplateContext) Deadline() (deadline time.Time, ok bool) { | |||
return c.parentContext().Deadline() | |||
} | |||
func (c TemplateContext) Done() <-chan struct{} { | |||
return c.parentContext().Done() | |||
} | |||
func (c TemplateContext) Err() error { | |||
return c.parentContext().Err() | |||
} | |||
func (c TemplateContext) Value(key any) any { | |||
return c.parentContext().Value(key) | |||
} | |||
// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context | |||
// as the current template's rendering context (request context), to help to find data race issues as early as possible. | |||
// When the code is proven to be correct and stable, this function should be removed. | |||
func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) { | |||
if c.parentContext() != dataCtx { | |||
log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2)) | |||
return "", errors.New("parent context mismatch") | |||
} | |||
return "", nil | |||
} |
@@ -28,6 +28,8 @@ import ( | |||
// NewFuncMap returns functions for injecting to templates | |||
func NewFuncMap() template.FuncMap { | |||
return map[string]any{ | |||
"ctx": func() any { return nil }, // template context function | |||
"DumpVar": dumpVar, | |||
// ----------------------------------------------------------------- |
@@ -6,6 +6,7 @@ package templates | |||
import ( | |||
"bufio" | |||
"bytes" | |||
"context" | |||
"errors" | |||
"fmt" | |||
"io" | |||
@@ -39,27 +40,28 @@ var ( | |||
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") | |||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any) error { | |||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive | |||
if respWriter, ok := w.(http.ResponseWriter); ok { | |||
if respWriter.Header().Get("Content-Type") == "" { | |||
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8") | |||
} | |||
respWriter.WriteHeader(status) | |||
} | |||
t, err := h.TemplateLookup(name) | |||
t, err := h.TemplateLookup(name, ctx) | |||
if err != nil { | |||
return texttemplate.ExecError{Name: name, Err: err} | |||
} | |||
return t.Execute(w, data) | |||
} | |||
func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) { | |||
func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive | |||
tmpls := h.templates.Load() | |||
if tmpls == nil { | |||
return nil, ErrTemplateNotInitialized | |||
} | |||
return tmpls.Executor(name, NewFuncMap()) | |||
m := NewFuncMap() | |||
m["ctx"] = func() any { return ctx } | |||
return tmpls.Executor(name, m) | |||
} | |||
func (h *HTMLRender) CompileTemplates() error { |
@@ -150,11 +150,11 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) { | |||
type mockRender struct{} | |||
func (tr *mockRender) TemplateLookup(tmpl string) (templates.TemplateExecutor, error) { | |||
func (tr *mockRender) TemplateLookup(tmpl string, _ gocontext.Context) (templates.TemplateExecutor, error) { | |||
return nil, nil | |||
} | |||
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ any) error { | |||
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ any, _ gocontext.Context) error { | |||
if resp, ok := w.(http.ResponseWriter); ok { | |||
resp.WriteHeader(status) | |||
} |
@@ -48,7 +48,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { | |||
data["ErrorMsg"] = "PANIC: " + combinedErr | |||
} | |||
err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, string(tplStatus500), data) | |||
err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, string(tplStatus500), data, nil) | |||
if err != nil { | |||
log.Error("Error occurs again when rendering error page: %v", err) | |||
w.WriteHeader(http.StatusInternalServerError) |
@@ -68,9 +68,13 @@ func Contexter() func(next http.Handler) http.Handler { | |||
} | |||
defer baseCleanUp() | |||
ctx.TemplateContext = context.NewTemplateContext(ctx) | |||
ctx.TemplateContext["Locale"] = ctx.Locale | |||
ctx.AppendContextValue(context.WebContextKey, ctx) | |||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) | |||
ctx.Data.MergeFrom(middleware.ContextData{ | |||
"Context": ctx, // TODO: use "ctx" in template and remove this | |||
"locale": ctx.Locale, | |||
"Title": ctx.Locale.Tr("install.install"), | |||
"PageIsInstall": true, |
@@ -578,7 +578,7 @@ func GrantApplicationOAuth(ctx *context.Context) { | |||
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities | |||
func OIDCWellKnown(ctx *context.Context) { | |||
t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown") | |||
t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil) | |||
if err != nil { | |||
ctx.ServerError("unable to find template", err) | |||
return |
@@ -13,7 +13,7 @@ const tplSwaggerV1Json base.TplName = "swagger/v1_json" | |||
// SwaggerV1Json render swagger v1 json | |||
func SwaggerV1Json(ctx *context.Context) { | |||
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json)) | |||
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil) | |||
if err != nil { | |||
ctx.ServerError("unable to find template", err) | |||
return |
@@ -26,6 +26,8 @@ | |||
{{end}} | |||
{{end}} | |||
<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script> | |||
{{template "custom/footer" .}} | |||
{{template "custom/footer" .}} | |||
{{ctx.DataRaceCheck $.Context}} | |||
</body> | |||
</html> |
@@ -1,5 +1,5 @@ | |||
<!DOCTYPE html> | |||
<html lang="{{.locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}"> | |||
<html lang="{{ctx.Locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}"> | |||
<head> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title> | |||
@@ -28,7 +28,7 @@ | |||
{{if .PageIsUserProfile}} | |||
<meta property="og:title" content="{{.ContextUser.DisplayName}}"> | |||
<meta property="og:type" content="profile"> | |||
<meta property="og:image" content="{{.ContextUser.AvatarLink $.Context}}"> | |||
<meta property="og:image" content="{{.ContextUser.AvatarLink ctx}}"> | |||
<meta property="og:url" content="{{.ContextUser.HTMLURL}}"> | |||
{{if .ContextUser.Description}} | |||
<meta property="og:description" content="{{.ContextUser.Description}}"> | |||
@@ -48,10 +48,10 @@ | |||
{{end}} | |||
{{end}} | |||
<meta property="og:type" content="object"> | |||
{{if (.Repository.AvatarLink $.Context)}} | |||
<meta property="og:image" content="{{.Repository.AvatarLink $.Context}}"> | |||
{{if (.Repository.AvatarLink ctx)}} | |||
<meta property="og:image" content="{{.Repository.AvatarLink ctx}}"> | |||
{{else}} | |||
<meta property="og:image" content="{{.Repository.Owner.AvatarLink $.Context}}"> | |||
<meta property="og:image" content="{{.Repository.Owner.AvatarLink ctx}}"> | |||
{{end}} | |||
{{else}} | |||
<meta property="og:title" content="{{AppName}}"> | |||
@@ -65,10 +65,11 @@ | |||
{{template "custom/header" .}} | |||
</head> | |||
<body> | |||
{{ctx.DataRaceCheck $.Context}} | |||
{{template "custom/body_outer_pre" .}} | |||
<div class="full height"> | |||
<noscript>{{.locale.Tr "enable_javascript"}}</noscript> | |||
<noscript>{{ctx.Locale.Tr "enable_javascript"}}</noscript> | |||
{{template "custom/body_inner_pre" .}} | |||