gitea源码

htmlrenderer.go 8.9KB

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