|
| 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