Start using template context function (#26254)
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.
This commit is contained in:
parent
0c6ae61229
commit
6913053223
12 changed files with 91 additions and 22 deletions
|
@ -5,6 +5,7 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
@ -31,14 +32,16 @@ import (
|
||||||
|
|
||||||
// Render represents a template render
|
// Render represents a template render
|
||||||
type Render interface {
|
type Render interface {
|
||||||
TemplateLookup(tmpl string) (templates.TemplateExecutor, error)
|
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
|
||||||
HTML(w io.Writer, status int, name string, data any) error
|
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context represents context of a request.
|
// Context represents context of a request.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
*Base
|
*Base
|
||||||
|
|
||||||
|
TemplateContext TemplateContext
|
||||||
|
|
||||||
Render Render
|
Render Render
|
||||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
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
|
Package *Package
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TemplateContext map[string]any
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||||
return req.Context().Value(WebContextKey).(*Context)
|
return req.Context().Value(WebContextKey).(*Context)
|
||||||
|
@ -133,8 +138,12 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
defer baseCleanUp()
|
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.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["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
|
||||||
ctx.Data["Link"] = ctx.Link
|
ctx.Data["Link"] = ctx.Link
|
||||||
ctx.Data["locale"] = ctx.Locale
|
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"
|
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 {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||||
// RenderToString renders the template content to a string
|
// RenderToString renders the template content to a string
|
||||||
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
|
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
|
||||||
var buf strings.Builder
|
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
|
return buf.String(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
49
modules/context/context_template.go
Normal file
49
modules/context/context_template.go
Normal file
|
@ -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
|
// NewFuncMap returns functions for injecting to templates
|
||||||
func NewFuncMap() template.FuncMap {
|
func NewFuncMap() template.FuncMap {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
|
"ctx": func() any { return nil }, // template context function
|
||||||
|
|
||||||
"DumpVar": dumpVar,
|
"DumpVar": dumpVar,
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
|
|
@ -6,6 +6,7 @@ package templates
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -39,27 +40,28 @@ var (
|
||||||
|
|
||||||
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
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, ok := w.(http.ResponseWriter); ok {
|
||||||
if respWriter.Header().Get("Content-Type") == "" {
|
if respWriter.Header().Get("Content-Type") == "" {
|
||||||
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
}
|
}
|
||||||
respWriter.WriteHeader(status)
|
respWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
t, err := h.TemplateLookup(name)
|
t, err := h.TemplateLookup(name, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return texttemplate.ExecError{Name: name, Err: err}
|
return texttemplate.ExecError{Name: name, Err: err}
|
||||||
}
|
}
|
||||||
return t.Execute(w, data)
|
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()
|
tmpls := h.templates.Load()
|
||||||
if tmpls == nil {
|
if tmpls == nil {
|
||||||
return nil, ErrTemplateNotInitialized
|
return nil, ErrTemplateNotInitialized
|
||||||
}
|
}
|
||||||
|
m := NewFuncMap()
|
||||||
return tmpls.Executor(name, NewFuncMap())
|
m["ctx"] = func() any { return ctx }
|
||||||
|
return tmpls.Executor(name, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTMLRender) CompileTemplates() error {
|
func (h *HTMLRender) CompileTemplates() error {
|
||||||
|
|
|
@ -150,11 +150,11 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
|
||||||
|
|
||||||
type mockRender struct{}
|
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
|
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 {
|
if resp, ok := w.(http.ResponseWriter); ok {
|
||||||
resp.WriteHeader(status)
|
resp.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
|
||||||
data["ErrorMsg"] = "PANIC: " + combinedErr
|
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 {
|
if err != nil {
|
||||||
log.Error("Error occurs again when rendering error page: %v", err)
|
log.Error("Error occurs again when rendering error page: %v", err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
|
@ -68,9 +68,13 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
defer baseCleanUp()
|
defer baseCleanUp()
|
||||||
|
|
||||||
|
ctx.TemplateContext = context.NewTemplateContext(ctx)
|
||||||
|
ctx.TemplateContext["Locale"] = ctx.Locale
|
||||||
|
|
||||||
ctx.AppendContextValue(context.WebContextKey, ctx)
|
ctx.AppendContextValue(context.WebContextKey, ctx)
|
||||||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
||||||
ctx.Data.MergeFrom(middleware.ContextData{
|
ctx.Data.MergeFrom(middleware.ContextData{
|
||||||
|
"Context": ctx, // TODO: use "ctx" in template and remove this
|
||||||
"locale": ctx.Locale,
|
"locale": ctx.Locale,
|
||||||
"Title": ctx.Locale.Tr("install.install"),
|
"Title": ctx.Locale.Tr("install.install"),
|
||||||
"PageIsInstall": true,
|
"PageIsInstall": true,
|
||||||
|
|
|
@ -578,7 +578,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
|
||||||
|
|
||||||
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
||||||
func OIDCWellKnown(ctx *context.Context) {
|
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 {
|
if err != nil {
|
||||||
ctx.ServerError("unable to find template", err)
|
ctx.ServerError("unable to find template", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,7 +13,7 @@ const tplSwaggerV1Json base.TplName = "swagger/v1_json"
|
||||||
|
|
||||||
// SwaggerV1Json render swagger v1 json
|
// SwaggerV1Json render swagger v1 json
|
||||||
func SwaggerV1Json(ctx *context.Context) {
|
func SwaggerV1Json(ctx *context.Context) {
|
||||||
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
|
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("unable to find template", err)
|
ctx.ServerError("unable to find template", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{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>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!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>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
|
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
{{if .PageIsUserProfile}}
|
{{if .PageIsUserProfile}}
|
||||||
<meta property="og:title" content="{{.ContextUser.DisplayName}}">
|
<meta property="og:title" content="{{.ContextUser.DisplayName}}">
|
||||||
<meta property="og:type" content="profile">
|
<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}}">
|
<meta property="og:url" content="{{.ContextUser.HTMLURL}}">
|
||||||
{{if .ContextUser.Description}}
|
{{if .ContextUser.Description}}
|
||||||
<meta property="og:description" content="{{.ContextUser.Description}}">
|
<meta property="og:description" content="{{.ContextUser.Description}}">
|
||||||
|
@ -48,10 +48,10 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<meta property="og:type" content="object">
|
<meta property="og:type" content="object">
|
||||||
{{if (.Repository.AvatarLink $.Context)}}
|
{{if (.Repository.AvatarLink ctx)}}
|
||||||
<meta property="og:image" content="{{.Repository.AvatarLink $.Context}}">
|
<meta property="og:image" content="{{.Repository.AvatarLink ctx}}">
|
||||||
{{else}}
|
{{else}}
|
||||||
<meta property="og:image" content="{{.Repository.Owner.AvatarLink $.Context}}">
|
<meta property="og:image" content="{{.Repository.Owner.AvatarLink ctx}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<meta property="og:title" content="{{AppName}}">
|
<meta property="og:title" content="{{AppName}}">
|
||||||
|
@ -65,10 +65,11 @@
|
||||||
{{template "custom/header" .}}
|
{{template "custom/header" .}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{{ctx.DataRaceCheck $.Context}}
|
||||||
{{template "custom/body_outer_pre" .}}
|
{{template "custom/body_outer_pre" .}}
|
||||||
|
|
||||||
<div class="full height">
|
<div class="full height">
|
||||||
<noscript>{{.locale.Tr "enable_javascript"}}</noscript>
|
<noscript>{{ctx.Locale.Tr "enable_javascript"}}</noscript>
|
||||||
|
|
||||||
{{template "custom/body_inner_pre" .}}
|
{{template "custom/body_inner_pre" .}}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue