Skip to content

Commit 2077de4

Browse files
committed
introduce ScopedTemplate
1 parent cb15364 commit 2077de4

File tree

5 files changed

+350
-16
lines changed

5 files changed

+350
-16
lines changed

modules/context/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import (
4747

4848
// Render represents a template render
4949
type Render interface {
50-
TemplateLookup(tmpl string) (*template.Template, error)
50+
TemplateLookup(tmpl string) (templates.TemplateExecutor, error)
5151
HTML(w io.Writer, status int, name string, data interface{}) error
5252
}
5353

modules/templates/htmlrenderer.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11-
"html/template"
1211
"io"
1312
"net/http"
1413
"path/filepath"
@@ -20,7 +19,7 @@ import (
2019

2120
"code.gitea.io/gitea/modules/log"
2221
"code.gitea.io/gitea/modules/setting"
23-
"code.gitea.io/gitea/modules/util"
22+
"code.gitea.io/gitea/modules/templates/scopedtmpl"
2423
"code.gitea.io/gitea/modules/watcher"
2524
)
2625

@@ -33,8 +32,10 @@ var (
3332
expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
3433
)
3534

35+
type TemplateExecutor scopedtmpl.TemplateExecutor
36+
3637
type HTMLRender struct {
37-
templates atomic.Pointer[template.Template]
38+
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
3839
}
3940

4041
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
@@ -53,32 +54,27 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}
5354
return t.Execute(w, data)
5455
}
5556

56-
func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
57+
func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) {
5758
tmpls := h.templates.Load()
5859
if tmpls == nil {
5960
return nil, ErrTemplateNotInitialized
6061
}
61-
tmpl := tmpls.Lookup(name)
62-
if tmpl == nil {
63-
return nil, util.ErrNotExist
64-
}
65-
return tmpl, nil
62+
63+
return tmpls.Executor(name, NewFuncMap()[0])
6664
}
6765

6866
func (h *HTMLRender) CompileTemplates() error {
6967
dirPrefix := "templates/"
7068
extSuffix := ".tmpl"
71-
tmpls := template.New("")
69+
tmpls := scopedtmpl.NewScopedTemplate()
70+
tmpls.Funcs(NewFuncMap()[0])
7271
for _, path := range GetTemplateAssetNames() {
7372
if !strings.HasSuffix(path, extSuffix) {
7473
continue
7574
}
7675
name := strings.TrimPrefix(path, dirPrefix)
7776
name = strings.TrimSuffix(name, extSuffix)
7877
tmpl := tmpls.New(filepath.ToSlash(name))
79-
for _, fm := range NewFuncMap() {
80-
tmpl.Funcs(fm)
81-
}
8278
buf, err := GetAsset(path)
8379
if err != nil {
8480
return err
@@ -87,6 +83,7 @@ func (h *HTMLRender) CompileTemplates() error {
8783
return err
8884
}
8985
}
86+
tmpls.Freeze()
9087
h.templates.Store(tmpls)
9188
return nil
9289
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package scopedtmpl
5+
6+
import (
7+
"fmt"
8+
"html/template"
9+
"io"
10+
"reflect"
11+
"sync"
12+
texttemplate "text/template"
13+
"text/template/parse"
14+
"unsafe"
15+
)
16+
17+
type TemplateExecutor interface {
18+
Execute(wr io.Writer, data interface{}) error
19+
}
20+
21+
type ScopedTemplate struct {
22+
all *template.Template
23+
parseFuncs template.FuncMap // this func map is only used for parsing templates
24+
frozen bool
25+
26+
scopedMu sync.RWMutex
27+
scopedTemplateSets map[string]*scopedTemplateSet
28+
}
29+
30+
func NewScopedTemplate() *ScopedTemplate {
31+
return &ScopedTemplate{
32+
all: template.New(""),
33+
parseFuncs: template.FuncMap{},
34+
scopedTemplateSets: map[string]*scopedTemplateSet{},
35+
}
36+
}
37+
38+
func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) {
39+
if t.frozen {
40+
panic("cannot add new functions to frozen template set")
41+
}
42+
t.all.Funcs(funcMap)
43+
for k, v := range funcMap {
44+
t.parseFuncs[k] = v
45+
}
46+
}
47+
48+
func (t *ScopedTemplate) New(name string) *template.Template {
49+
if t.frozen {
50+
panic("cannot add new template to frozen template set")
51+
}
52+
return t.all.New(name)
53+
}
54+
55+
func (t *ScopedTemplate) Freeze() {
56+
t.frozen = true
57+
// reset the exec func map, then `escapeTemplate` is safe to call `Execute` to do escaping
58+
m := template.FuncMap{}
59+
for k := range t.parseFuncs {
60+
m[k] = func(v ...any) any { return nil }
61+
}
62+
t.all.Funcs(m)
63+
}
64+
65+
func (t *ScopedTemplate) Executor(name string, funcMap template.FuncMap) (TemplateExecutor, error) {
66+
t.scopedMu.RLock()
67+
scopedTmplSet, ok := t.scopedTemplateSets[name]
68+
t.scopedMu.RUnlock()
69+
70+
if !ok {
71+
var err error
72+
t.scopedMu.Lock()
73+
if scopedTmplSet, ok = t.scopedTemplateSets[name]; !ok {
74+
if scopedTmplSet, err = newScopedTemplateSet(t.all, name); err == nil {
75+
t.scopedTemplateSets[name] = scopedTmplSet
76+
}
77+
}
78+
t.scopedMu.Unlock()
79+
if err != nil {
80+
return nil, err
81+
}
82+
}
83+
84+
if scopedTmplSet == nil {
85+
return nil, fmt.Errorf("template %s not found", name)
86+
}
87+
return scopedTmplSet.newExecutor(funcMap), nil
88+
}
89+
90+
type scopedTemplateSet struct {
91+
name string
92+
htmlTemplates map[string]*template.Template
93+
textTemplates map[string]*texttemplate.Template
94+
execFuncs map[string]reflect.Value
95+
}
96+
97+
func escapeTemplate(t *template.Template) error {
98+
// force the Golang HTML template to complete the escaping work
99+
err := t.Execute(io.Discard, nil)
100+
if _, ok := err.(*template.Error); ok {
101+
return err
102+
}
103+
return nil
104+
}
105+
106+
//nolint:unused
107+
type htmlTemplate struct {
108+
escapeErr error
109+
text *texttemplate.Template
110+
}
111+
112+
//nolint:unused
113+
type textTemplateCommon struct {
114+
tmpl map[string]*template.Template // Map from name to defined templates.
115+
muTmpl sync.RWMutex // protects tmpl
116+
option struct {
117+
missingKey int
118+
}
119+
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
120+
parseFuncs texttemplate.FuncMap
121+
execFuncs map[string]reflect.Value
122+
}
123+
124+
//nolint:unused
125+
type textTemplate struct {
126+
name string
127+
*parse.Tree
128+
*textTemplateCommon
129+
leftDelim string
130+
rightDelim string
131+
}
132+
133+
func ptr[T, P any](ptr *P) *T {
134+
// https://pkg.go.dev/unsafe#Pointer
135+
// (1) Conversion of a *T1 to Pointer to *T2.
136+
// Provided that T2 is no larger than T1 and that the two share an equivalent memory layout,
137+
// this conversion allows reinterpreting data of one type as data of another type.
138+
return (*T)(unsafe.Pointer(ptr))
139+
}
140+
141+
func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateSet, error) {
142+
targetTmpl := all.Lookup(name)
143+
if targetTmpl == nil {
144+
return nil, fmt.Errorf("template %q not found", name)
145+
}
146+
if err := escapeTemplate(targetTmpl); err != nil {
147+
return nil, fmt.Errorf("template %q has an error when escaping: %v", name, err)
148+
}
149+
150+
ts := &scopedTemplateSet{
151+
name: name,
152+
htmlTemplates: map[string]*template.Template{},
153+
textTemplates: map[string]*texttemplate.Template{},
154+
}
155+
156+
htmlTmpl := ptr[htmlTemplate](all)
157+
textTmpl := htmlTmpl.text
158+
textTmplPtr := ptr[textTemplate](textTmpl)
159+
160+
textTmplPtr.muFuncs.Lock()
161+
ts.execFuncs = map[string]reflect.Value{}
162+
for k, v := range textTmplPtr.execFuncs {
163+
ts.execFuncs[k] = v
164+
}
165+
textTmplPtr.muFuncs.Unlock()
166+
167+
var collectTemplates func(nodes []parse.Node)
168+
var collectErr error // only need to collect the one error
169+
collectTemplates = func(nodes []parse.Node) {
170+
for _, node := range nodes {
171+
if node.Type() == parse.NodeTemplate {
172+
nodeTemplate := node.(*parse.TemplateNode)
173+
subName := nodeTemplate.Name
174+
if ts.htmlTemplates[subName] == nil {
175+
subTmpl := all.Lookup(subName)
176+
if subTmpl == nil {
177+
// HTML template will add some internal templates like "$delimDoubleQuote" into the text template
178+
ts.textTemplates[subName] = textTmpl.Lookup(subName)
179+
} else if subTmpl.Tree == nil || subTmpl.Tree.Root == nil {
180+
collectErr = fmt.Errorf("template %q has no tree, it's usually caused by broken templates", subName)
181+
} else {
182+
ts.htmlTemplates[subName] = subTmpl
183+
if err := escapeTemplate(subTmpl); err != nil {
184+
collectErr = fmt.Errorf("template %q has an error when escaping: %v", subName, err)
185+
return
186+
}
187+
collectTemplates(subTmpl.Tree.Root.Nodes)
188+
}
189+
}
190+
} else if node.Type() == parse.NodeList {
191+
nodeList := node.(*parse.ListNode)
192+
collectTemplates(nodeList.Nodes)
193+
} else if node.Type() == parse.NodeIf {
194+
nodeIf := node.(*parse.IfNode)
195+
collectTemplates(nodeIf.BranchNode.List.Nodes)
196+
if nodeIf.BranchNode.ElseList != nil {
197+
collectTemplates(nodeIf.BranchNode.ElseList.Nodes)
198+
}
199+
} else if node.Type() == parse.NodeRange {
200+
nodeRange := node.(*parse.RangeNode)
201+
collectTemplates(nodeRange.BranchNode.List.Nodes)
202+
if nodeRange.BranchNode.ElseList != nil {
203+
collectTemplates(nodeRange.BranchNode.ElseList.Nodes)
204+
}
205+
} else if node.Type() == parse.NodeWith {
206+
nodeWith := node.(*parse.WithNode)
207+
collectTemplates(nodeWith.BranchNode.List.Nodes)
208+
if nodeWith.BranchNode.ElseList != nil {
209+
collectTemplates(nodeWith.BranchNode.ElseList.Nodes)
210+
}
211+
}
212+
}
213+
}
214+
ts.htmlTemplates[name] = targetTmpl
215+
collectTemplates(targetTmpl.Tree.Root.Nodes)
216+
return ts, collectErr
217+
}
218+
219+
func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecutor {
220+
tmpl := texttemplate.New("")
221+
tmplPtr := ptr[textTemplate](tmpl)
222+
tmplPtr.execFuncs = map[string]reflect.Value{}
223+
for k, v := range ts.execFuncs {
224+
tmplPtr.execFuncs[k] = v
225+
}
226+
if funcMap != nil {
227+
tmpl.Funcs(funcMap)
228+
}
229+
// after escapeTemplate, the html templates are also escaped text templates, so it could be added to the text template directly
230+
for _, t := range ts.htmlTemplates {
231+
_, _ = tmpl.AddParseTree(t.Name(), t.Tree)
232+
}
233+
for _, t := range ts.textTemplates {
234+
_, _ = tmpl.AddParseTree(t.Name(), t.Tree)
235+
}
236+
237+
// now the text template has all necessary escaped templates, so we can safely execute, just like what the html template does
238+
return tmpl.Lookup(ts.name)
239+
}

0 commit comments

Comments
 (0)