2022-08-28 12:43:25 +03:00
// Copyright 2022 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2022-08-28 12:43:25 +03:00
package templates
import (
2023-04-14 08:19:11 +03:00
"bufio"
2022-10-08 00:02:24 +03:00
"bytes"
2022-08-28 12:43:25 +03:00
"context"
2023-04-08 16:15:22 +03:00
"errors"
2022-10-08 00:02:24 +03:00
"fmt"
2023-04-08 09:21:50 +03:00
"html/template"
"io"
"net/http"
"path/filepath"
2022-10-08 00:02:24 +03:00
"regexp"
"strconv"
"strings"
2023-04-08 09:21:50 +03:00
"sync/atomic"
2023-04-08 16:15:22 +03:00
texttemplate "text/template"
2022-08-28 12:43:25 +03:00
2023-04-14 08:19:11 +03:00
"code.gitea.io/gitea/modules/assetfs"
2022-08-28 12:43:25 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2023-04-08 16:15:22 +03:00
"code.gitea.io/gitea/modules/util"
2022-08-28 12:43:25 +03:00
)
2023-04-14 08:19:11 +03:00
var rendererKey interface { } = "templatesHtmlRenderer"
2022-08-28 12:43:25 +03:00
2023-04-08 09:21:50 +03:00
type HTMLRender struct {
templates atomic . Pointer [ template . Template ]
}
2023-04-08 16:15:22 +03:00
var ErrTemplateNotInitialized = errors . New ( "template system is not initialized, check your log for errors" )
2023-04-08 09:21:50 +03:00
func ( h * HTMLRender ) HTML ( w io . Writer , status int , name string , data interface { } ) error {
if respWriter , ok := w . ( http . ResponseWriter ) ; ok {
if respWriter . Header ( ) . Get ( "Content-Type" ) == "" {
respWriter . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2022-08-28 12:43:25 +03:00
}
2023-04-08 09:21:50 +03:00
respWriter . WriteHeader ( status )
}
2023-04-08 16:15:22 +03:00
t , err := h . TemplateLookup ( name )
if err != nil {
return texttemplate . ExecError { Name : name , Err : err }
}
return t . Execute ( w , data )
2023-04-08 09:21:50 +03:00
}
2023-04-08 16:15:22 +03:00
func ( h * HTMLRender ) TemplateLookup ( name string ) ( * template . Template , error ) {
tmpls := h . templates . Load ( )
if tmpls == nil {
return nil , ErrTemplateNotInitialized
}
tmpl := tmpls . Lookup ( name )
if tmpl == nil {
return nil , util . ErrNotExist
}
return tmpl , nil
2023-04-08 09:21:50 +03:00
}
func ( h * HTMLRender ) CompileTemplates ( ) error {
2023-04-08 16:56:50 +03:00
extSuffix := ".tmpl"
2023-04-08 09:21:50 +03:00
tmpls := template . New ( "" )
2023-04-12 13:16:45 +03:00
assets := AssetFS ( )
files , err := ListWebTemplateAssetNames ( assets )
if err != nil {
return nil
}
for _ , file := range files {
if ! strings . HasSuffix ( file , extSuffix ) {
2023-04-08 16:56:50 +03:00
continue
}
2023-04-12 13:16:45 +03:00
name := strings . TrimSuffix ( file , extSuffix )
2023-04-08 09:21:50 +03:00
tmpl := tmpls . New ( filepath . ToSlash ( name ) )
for _ , fm := range NewFuncMap ( ) {
tmpl . Funcs ( fm )
}
2023-04-12 13:16:45 +03:00
buf , err := assets . ReadFile ( file )
2023-04-08 09:21:50 +03:00
if err != nil {
return err
}
if _ , err = tmpl . Parse ( string ( buf ) ) ; err != nil {
return err
}
}
h . templates . Store ( tmpls )
return nil
}
// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
func HTMLRenderer ( ctx context . Context ) ( context . Context , * HTMLRender ) {
if renderer , ok := ctx . Value ( rendererKey ) . ( * HTMLRender ) ; ok {
return ctx , renderer
2022-08-28 12:43:25 +03:00
}
rendererType := "static"
if ! setting . IsProd {
rendererType = "auto-reloading"
}
log . Log ( 1 , log . DEBUG , "Creating " + rendererType + " HTML Renderer" )
2023-04-08 09:21:50 +03:00
renderer := & HTMLRender { }
if err := renderer . CompileTemplates ( ) ; err != nil {
2023-04-14 08:19:11 +03:00
p := & templateErrorPrettier { assets : AssetFS ( ) }
wrapFatal ( p . handleFuncNotDefinedError ( err ) )
wrapFatal ( p . handleUnexpectedOperandError ( err ) )
wrapFatal ( p . handleExpectedEndError ( err ) )
wrapFatal ( p . handleGenericTemplateError ( err ) )
log . Fatal ( "HTMLRenderer CompileTemplates error: %v" , err )
2023-04-08 09:21:50 +03:00
}
2022-08-28 12:43:25 +03:00
if ! setting . IsProd {
2023-04-12 13:16:45 +03:00
go AssetFS ( ) . WatchLocalChanges ( ctx , func ( ) {
if err := renderer . CompileTemplates ( ) ; err != nil {
log . Error ( "Template error: %v\n%s" , err , log . Stack ( 2 ) )
}
2022-08-28 12:43:25 +03:00
} )
}
return context . WithValue ( ctx , rendererKey , renderer ) , renderer
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func wrapFatal ( msg string ) {
if msg == "" {
2022-10-08 00:02:24 +03:00
return
}
2023-04-14 08:19:11 +03:00
log . FatalWithSkip ( 1 , "Unable to compile templates, %s" , msg )
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
type templateErrorPrettier struct {
assets * assetfs . LayeredFS
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
var reGenericTemplateError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): (.*) ` )
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) handleGenericTemplateError ( err error ) string {
groups := reGenericTemplateError . FindStringSubmatch ( err . Error ( ) )
2022-10-08 00:02:24 +03:00
if len ( groups ) != 4 {
2023-04-14 08:19:11 +03:00
return ""
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
tmplName , lineStr , message := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
return p . makeDetailedError ( message , tmplName , lineStr , - 1 , "" )
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
var reFuncNotDefinedError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): (function "(.*)" not defined) ` )
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) handleFuncNotDefinedError ( err error ) string {
groups := reFuncNotDefinedError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 5 {
return ""
}
tmplName , lineStr , message , funcName := groups [ 1 ] , groups [ 2 ] , groups [ 3 ] , groups [ 4 ]
funcName , _ = strconv . Unquote ( ` " ` + funcName + ` " ` )
return p . makeDetailedError ( message , tmplName , lineStr , - 1 , funcName )
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
var reUnexpectedOperandError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): (unexpected "(.*)" in operand) ` )
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) handleUnexpectedOperandError ( err error ) string {
groups := reUnexpectedOperandError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 5 {
return ""
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
tmplName , lineStr , message , unexpected := groups [ 1 ] , groups [ 2 ] , groups [ 3 ] , groups [ 4 ]
unexpected , _ = strconv . Unquote ( ` " ` + unexpected + ` " ` )
return p . makeDetailedError ( message , tmplName , lineStr , - 1 , unexpected )
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
var reExpectedEndError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): (expected end; found (.*)) ` )
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) handleExpectedEndError ( err error ) string {
groups := reExpectedEndError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 5 {
return ""
}
tmplName , lineStr , message , unexpected := groups [ 1 ] , groups [ 2 ] , groups [ 3 ] , groups [ 4 ]
return p . makeDetailedError ( message , tmplName , lineStr , - 1 , unexpected )
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
var (
reTemplateExecutingError = regexp . MustCompile ( ` ^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*) ` )
reTemplateExecutingErrorMsg = regexp . MustCompile ( ` ^executing "(.*)" at <(.*)>: ` )
)
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) handleTemplateRenderingError ( err error ) string {
if groups := reTemplateExecutingError . FindStringSubmatch ( err . Error ( ) ) ; len ( groups ) > 0 {
tmplName , lineStr , posStr , msgPart := groups [ 1 ] , groups [ 2 ] , groups [ 3 ] , groups [ 4 ]
target := ""
if groups = reTemplateExecutingErrorMsg . FindStringSubmatch ( msgPart ) ; len ( groups ) > 0 {
target = groups [ 2 ]
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
return p . makeDetailedError ( msgPart , tmplName , lineStr , posStr , target )
} else if execErr , ok := err . ( texttemplate . ExecError ) ; ok {
layerName := p . assets . GetFileLayerName ( execErr . Name + ".tmpl" )
return fmt . Sprintf ( "asset from: %s, %s" , layerName , err . Error ( ) )
} else {
return err . Error ( )
}
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func HandleTemplateRenderingError ( err error ) string {
p := & templateErrorPrettier { assets : AssetFS ( ) }
return p . handleTemplateRenderingError ( err )
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
const dashSeparator = "----------------------------------------------------------------------"
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
func ( p * templateErrorPrettier ) makeDetailedError ( errMsg , tmplName string , lineNum , posNum any , target string ) string {
code , layer , err := p . assets . ReadLayeredFile ( tmplName + ".tmpl" )
if err != nil {
return fmt . Sprintf ( "template error: %s, and unable to find template file %q" , errMsg , tmplName )
}
line , err := util . ToInt64 ( lineNum )
if err != nil {
return fmt . Sprintf ( "template error: %s, unable to parse template %q line number %q" , errMsg , tmplName , lineNum )
}
pos , err := util . ToInt64 ( posNum )
if err != nil {
return fmt . Sprintf ( "template error: %s, unable to parse template %q pos number %q" , errMsg , tmplName , posNum )
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
detail := extractErrorLine ( code , int ( line ) , int ( pos ) , target )
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
var msg string
if pos >= 0 {
msg = fmt . Sprintf ( "template error: %s:%s:%d:%d : %s" , layer , tmplName , line , pos , errMsg )
} else {
msg = fmt . Sprintf ( "template error: %s:%s:%d : %s" , layer , tmplName , line , errMsg )
2023-03-20 23:56:48 +03:00
}
2023-04-14 08:19:11 +03:00
return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
}
func extractErrorLine ( code [ ] byte , lineNum , posNum int , target string ) string {
b := bufio . NewReader ( bytes . NewReader ( code ) )
var line [ ] byte
var err error
for i := 0 ; i < lineNum ; i ++ {
if line , err = b . ReadBytes ( '\n' ) ; err != nil {
if i == lineNum - 1 && errors . Is ( err , io . EOF ) {
err = nil
2022-10-08 00:02:24 +03:00
}
2023-04-14 08:19:11 +03:00
break
2023-03-20 23:56:48 +03:00
}
2023-04-14 08:19:11 +03:00
}
if err != nil {
return fmt . Sprintf ( "unable to find target line %d" , lineNum )
}
2022-10-08 00:02:24 +03:00
2023-04-14 08:19:11 +03:00
line = bytes . TrimRight ( line , "\r\n" )
var indicatorLine [ ] byte
targetBytes := [ ] byte ( target )
targetLen := len ( targetBytes )
for i := 0 ; i < len ( line ) ; {
if posNum == - 1 && target != "" && bytes . HasPrefix ( line [ i : ] , targetBytes ) {
for j := 0 ; j < targetLen && i < len ( line ) ; j ++ {
indicatorLine = append ( indicatorLine , '^' )
i ++
}
} else if i == posNum {
indicatorLine = append ( indicatorLine , '^' )
i ++
} else {
if line [ i ] == '\t' {
indicatorLine = append ( indicatorLine , '\t' )
} else {
indicatorLine = append ( indicatorLine , ' ' )
}
i ++
2022-10-08 00:02:24 +03:00
}
}
2023-04-14 08:19:11 +03:00
// if the indicatorLine only contains spaces, trim it together
return strings . TrimRight ( string ( line ) + "\n" + string ( indicatorLine ) , " \t\r\n" )
2022-10-08 00:02:24 +03:00
}