code.gitea.io/gitea@v1.22.3/modules/templates/scopedtmpl/scopedtmpl.go (about) 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 any) 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: %w", 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: %w", 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 }