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.2KB

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