code.gitea.io/gitea@v1.22.3/modules/templates/htmlrenderer.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package templates
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"sync/atomic"
    20  	texttemplate "text/template"
    21  
    22  	"code.gitea.io/gitea/modules/assetfs"
    23  	"code.gitea.io/gitea/modules/graceful"
    24  	"code.gitea.io/gitea/modules/log"
    25  	"code.gitea.io/gitea/modules/setting"
    26  	"code.gitea.io/gitea/modules/templates/scopedtmpl"
    27  	"code.gitea.io/gitea/modules/util"
    28  )
    29  
    30  type TemplateExecutor scopedtmpl.TemplateExecutor
    31  
    32  type HTMLRender struct {
    33  	templates atomic.Pointer[scopedtmpl.ScopedTemplate]
    34  }
    35  
    36  var (
    37  	htmlRender     *HTMLRender
    38  	htmlRenderOnce sync.Once
    39  )
    40  
    41  var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
    42  
    43  func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
    44  	if respWriter, ok := w.(http.ResponseWriter); ok {
    45  		if respWriter.Header().Get("Content-Type") == "" {
    46  			respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
    47  		}
    48  		respWriter.WriteHeader(status)
    49  	}
    50  	t, err := h.TemplateLookup(name, ctx)
    51  	if err != nil {
    52  		return texttemplate.ExecError{Name: name, Err: err}
    53  	}
    54  	return t.Execute(w, data)
    55  }
    56  
    57  func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive
    58  	tmpls := h.templates.Load()
    59  	if tmpls == nil {
    60  		return nil, ErrTemplateNotInitialized
    61  	}
    62  	m := NewFuncMap()
    63  	m["ctx"] = func() any { return ctx }
    64  	return tmpls.Executor(name, m)
    65  }
    66  
    67  func (h *HTMLRender) CompileTemplates() error {
    68  	assets := AssetFS()
    69  	extSuffix := ".tmpl"
    70  	tmpls := scopedtmpl.NewScopedTemplate()
    71  	tmpls.Funcs(NewFuncMap())
    72  	files, err := ListWebTemplateAssetNames(assets)
    73  	if err != nil {
    74  		return nil
    75  	}
    76  	for _, file := range files {
    77  		if !strings.HasSuffix(file, extSuffix) {
    78  			continue
    79  		}
    80  		name := strings.TrimSuffix(file, extSuffix)
    81  		tmpl := tmpls.New(filepath.ToSlash(name))
    82  		buf, err := assets.ReadFile(file)
    83  		if err != nil {
    84  			return err
    85  		}
    86  		if _, err = tmpl.Parse(string(buf)); err != nil {
    87  			return err
    88  		}
    89  	}
    90  	tmpls.Freeze()
    91  	h.templates.Store(tmpls)
    92  	return nil
    93  }
    94  
    95  // HTMLRenderer init once and returns the globally shared html renderer
    96  func HTMLRenderer() *HTMLRender {
    97  	htmlRenderOnce.Do(initHTMLRenderer)
    98  	return htmlRender
    99  }
   100  
   101  func ReloadHTMLTemplates() error {
   102  	log.Trace("Reloading HTML templates")
   103  	if err := htmlRender.CompileTemplates(); err != nil {
   104  		log.Error("Template error: %v\n%s", err, log.Stack(2))
   105  		return err
   106  	}
   107  	return nil
   108  }
   109  
   110  func initHTMLRenderer() {
   111  	rendererType := "static"
   112  	if !setting.IsProd {
   113  		rendererType = "auto-reloading"
   114  	}
   115  	log.Debug("Creating %s HTML Renderer", rendererType)
   116  
   117  	htmlRender = &HTMLRender{}
   118  	if err := htmlRender.CompileTemplates(); err != nil {
   119  		p := &templateErrorPrettier{assets: AssetFS()}
   120  		wrapTmplErrMsg(p.handleFuncNotDefinedError(err))
   121  		wrapTmplErrMsg(p.handleUnexpectedOperandError(err))
   122  		wrapTmplErrMsg(p.handleExpectedEndError(err))
   123  		wrapTmplErrMsg(p.handleGenericTemplateError(err))
   124  		wrapTmplErrMsg(fmt.Sprintf("CompileTemplates error: %v", err))
   125  	}
   126  
   127  	if !setting.IsProd {
   128  		go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
   129  			_ = ReloadHTMLTemplates()
   130  		})
   131  	}
   132  }
   133  
   134  func wrapTmplErrMsg(msg string) {
   135  	if msg == "" {
   136  		return
   137  	}
   138  	if setting.IsProd {
   139  		// in prod mode, Gitea must have correct templates to run
   140  		log.Fatal("Gitea can't run with template errors: %s", msg)
   141  	}
   142  	// in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded
   143  	log.Error("There are template errors but Gitea continues to run in dev mode: %s", msg)
   144  }
   145  
   146  type templateErrorPrettier struct {
   147  	assets *assetfs.LayeredFS
   148  }
   149  
   150  var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
   151  
   152  func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
   153  	groups := reGenericTemplateError.FindStringSubmatch(err.Error())
   154  	if len(groups) != 4 {
   155  		return ""
   156  	}
   157  	tmplName, lineStr, message := groups[1], groups[2], groups[3]
   158  	return p.makeDetailedError(message, tmplName, lineStr, -1, "")
   159  }
   160  
   161  var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
   162  
   163  func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
   164  	groups := reFuncNotDefinedError.FindStringSubmatch(err.Error())
   165  	if len(groups) != 5 {
   166  		return ""
   167  	}
   168  	tmplName, lineStr, message, funcName := groups[1], groups[2], groups[3], groups[4]
   169  	funcName, _ = strconv.Unquote(`"` + funcName + `"`)
   170  	return p.makeDetailedError(message, tmplName, lineStr, -1, funcName)
   171  }
   172  
   173  var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
   174  
   175  func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
   176  	groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
   177  	if len(groups) != 5 {
   178  		return ""
   179  	}
   180  	tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
   181  	unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
   182  	return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
   183  }
   184  
   185  var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
   186  
   187  func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
   188  	groups := reExpectedEndError.FindStringSubmatch(err.Error())
   189  	if len(groups) != 5 {
   190  		return ""
   191  	}
   192  	tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
   193  	return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
   194  }
   195  
   196  var (
   197  	reTemplateExecutingError    = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
   198  	reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
   199  )
   200  
   201  func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
   202  	if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
   203  		tmplName, lineStr, posStr, msgPart := groups[1], groups[2], groups[3], groups[4]
   204  		target := ""
   205  		if groups = reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
   206  			target = groups[2]
   207  		}
   208  		return p.makeDetailedError(msgPart, tmplName, lineStr, posStr, target)
   209  	} else if execErr, ok := err.(texttemplate.ExecError); ok {
   210  		layerName := p.assets.GetFileLayerName(execErr.Name + ".tmpl")
   211  		return fmt.Sprintf("asset from: %s, %s", layerName, err.Error())
   212  	}
   213  	return err.Error()
   214  }
   215  
   216  func HandleTemplateRenderingError(err error) string {
   217  	p := &templateErrorPrettier{assets: AssetFS()}
   218  	return p.handleTemplateRenderingError(err)
   219  }
   220  
   221  const dashSeparator = "----------------------------------------------------------------------"
   222  
   223  func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string {
   224  	code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl")
   225  	if err != nil {
   226  		return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName)
   227  	}
   228  	line, err := util.ToInt64(lineNum)
   229  	if err != nil {
   230  		return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum)
   231  	}
   232  	pos, err := util.ToInt64(posNum)
   233  	if err != nil {
   234  		return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum)
   235  	}
   236  	detail := extractErrorLine(code, int(line), int(pos), target)
   237  
   238  	var msg string
   239  	if pos >= 0 {
   240  		msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg)
   241  	} else {
   242  		msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg)
   243  	}
   244  	return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
   245  }
   246  
   247  func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
   248  	b := bufio.NewReader(bytes.NewReader(code))
   249  	var line []byte
   250  	var err error
   251  	for i := 0; i < lineNum; i++ {
   252  		if line, err = b.ReadBytes('\n'); err != nil {
   253  			if i == lineNum-1 && errors.Is(err, io.EOF) {
   254  				err = nil
   255  			}
   256  			break
   257  		}
   258  	}
   259  	if err != nil {
   260  		return fmt.Sprintf("unable to find target line %d", lineNum)
   261  	}
   262  
   263  	line = bytes.TrimRight(line, "\r\n")
   264  	var indicatorLine []byte
   265  	targetBytes := []byte(target)
   266  	targetLen := len(targetBytes)
   267  	for i := 0; i < len(line); {
   268  		if posNum == -1 && target != "" && bytes.HasPrefix(line[i:], targetBytes) {
   269  			for j := 0; j < targetLen && i < len(line); j++ {
   270  				indicatorLine = append(indicatorLine, '^')
   271  				i++
   272  			}
   273  		} else if i == posNum {
   274  			indicatorLine = append(indicatorLine, '^')
   275  			i++
   276  		} else {
   277  			if line[i] == '\t' {
   278  				indicatorLine = append(indicatorLine, '\t')
   279  			} else {
   280  				indicatorLine = append(indicatorLine, ' ')
   281  			}
   282  			i++
   283  		}
   284  	}
   285  	// if the indicatorLine only contains spaces, trim it together
   286  	return strings.TrimRight(string(line)+"\n"+string(indicatorLine), " \t\r\n")
   287  }