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  }