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 }