You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

htmlrenderer.go 8.7KB

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