// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync/atomic"
texttemplate "text/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/watcher"
)
var (
rendererKey interface { } = "templatesHtmlRenderer"
templateError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): (.*) ` )
notDefinedError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): function "(.*)" not defined ` )
unexpectedError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): unexpected "(.*)" in operand ` )
expectedEndError = regexp . MustCompile ( ` ^template: (.*):([0-9]+): expected end; found (.*) ` )
)
type HTMLRender struct {
templates atomic . Pointer [ template . Template ]
}
var ErrTemplateNotInitialized = errors . New ( "template system is not initialized, check your log for errors" )
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" )
}
respWriter . WriteHeader ( status )
}
t , err := h . TemplateLookup ( name )
if err != nil {
return texttemplate . ExecError { Name : name , Err : err }
}
return t . Execute ( w , data )
}
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
}
func ( h * HTMLRender ) CompileTemplates ( ) error {
dirPrefix := "templates/"
extSuffix := ".tmpl"
tmpls := template . New ( "" )
for _ , path := range GetTemplateAssetNames ( ) {
if ! strings . HasSuffix ( path , extSuffix ) {
continue
}
name := strings . TrimPrefix ( path , dirPrefix )
name = strings . TrimSuffix ( name , extSuffix )
tmpl := tmpls . New ( filepath . ToSlash ( name ) )
for _ , fm := range NewFuncMap ( ) {
tmpl . Funcs ( fm )
}
buf , err := GetAsset ( path )
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
}
rendererType := "static"
if ! setting . IsProd {
rendererType = "auto-reloading"
}
log . Log ( 1 , log . DEBUG , "Creating " + rendererType + " HTML Renderer" )
renderer := & HTMLRender { }
if err := renderer . CompileTemplates ( ) ; err != nil {
wrapFatal ( handleNotDefinedPanicError ( err ) )
wrapFatal ( handleUnexpected ( err ) )
wrapFatal ( handleExpectedEnd ( err ) )
wrapFatal ( handleGenericTemplateError ( err ) )
log . Fatal ( "HTMLRenderer error: %v" , err )
}
if ! setting . IsProd {
watcher . CreateWatcher ( ctx , "HTML Templates" , & watcher . CreateWatcherOpts {
PathsCallback : walkTemplateFiles ,
BetweenCallback : func ( ) {
if err := renderer . CompileTemplates ( ) ; err != nil {
log . Error ( "Template error: %v\n%s" , err , log . Stack ( 2 ) )
}
} ,
} )
}
return context . WithValue ( ctx , rendererKey , renderer ) , renderer
}
func wrapFatal ( format string , args [ ] interface { } ) {
if format == "" {
return
}
log . FatalWithSkip ( 1 , format , args ... )
}
func handleGenericTemplateError ( err error ) ( string , [ ] interface { } ) {
groups := templateError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 4 {
return "" , nil
}
templateName , lineNumberStr , message := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
filename , assetErr := GetAssetFilename ( "templates/" + templateName + ".tmpl" )
if assetErr != nil {
return "" , nil
}
lineNumber , _ := strconv . Atoi ( lineNumberStr )
line := GetLineFromTemplate ( templateName , lineNumber , "" , - 1 )
return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s" , [ ] interface { } { message , filename , lineNumber , log . NewColoredValue ( line , log . Reset ) , log . Stack ( 2 ) }
}
func handleNotDefinedPanicError ( err error ) ( string , [ ] interface { } ) {
groups := notDefinedError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 4 {
return "" , nil
}
templateName , lineNumberStr , functionName := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
functionName , _ = strconv . Unquote ( ` " ` + functionName + ` " ` )
filename , assetErr := GetAssetFilename ( "templates/" + templateName + ".tmpl" )
if assetErr != nil {
return "" , nil
}
lineNumber , _ := strconv . Atoi ( lineNumberStr )
line := GetLineFromTemplate ( templateName , lineNumber , functionName , - 1 )
return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s" , [ ] interface { } { functionName , filename , lineNumber , log . NewColoredValue ( line , log . Reset ) }
}
func handleUnexpected ( err error ) ( string , [ ] interface { } ) {
groups := unexpectedError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 4 {
return "" , nil
}
templateName , lineNumberStr , unexpected := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
unexpected , _ = strconv . Unquote ( ` " ` + unexpected + ` " ` )
filename , assetErr := GetAssetFilename ( "templates/" + templateName + ".tmpl" )
if assetErr != nil {
return "" , nil
}
lineNumber , _ := strconv . Atoi ( lineNumberStr )
line := GetLineFromTemplate ( templateName , lineNumber , unexpected , - 1 )
return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s" , [ ] interface { } { unexpected , filename , lineNumber , log . NewColoredValue ( line , log . Reset ) }
}
func handleExpectedEnd ( err error ) ( string , [ ] interface { } ) {
groups := expectedEndError . FindStringSubmatch ( err . Error ( ) )
if len ( groups ) != 4 {
return "" , nil
}
templateName , lineNumberStr , unexpected := groups [ 1 ] , groups [ 2 ] , groups [ 3 ]
filename , assetErr := GetAssetFilename ( "templates/" + templateName + ".tmpl" )
if assetErr != nil {
return "" , nil
}
lineNumber , _ := strconv . Atoi ( lineNumberStr )
line := GetLineFromTemplate ( templateName , lineNumber , unexpected , - 1 )
return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s" , [ ] interface { } { unexpected , filename , lineNumber , log . NewColoredValue ( line , log . Reset ) }
}
const dashSeparator = "----------------------------------------------------------------------\n"
// GetLineFromTemplate returns a line from a template with some context
func GetLineFromTemplate ( templateName string , targetLineNum int , target string , position int ) string {
bs , err := GetAsset ( "templates/" + templateName + ".tmpl" )
if err != nil {
return fmt . Sprintf ( "(unable to read template file: %v)" , err )
}
sb := & strings . Builder { }
// Write the header
sb . WriteString ( dashSeparator )
var lineBs [ ] byte
// Iterate through the lines from the asset file to find the target line
for start , currentLineNum := 0 , 1 ; currentLineNum <= targetLineNum && start < len ( bs ) ; currentLineNum ++ {
// Find the next new line
end := bytes . IndexByte ( bs [ start : ] , '\n' )
// adjust the end to be a direct pointer in to []byte
if end < 0 {
end = len ( bs )
} else {
end += start
}
// set lineBs to the current line []byte
lineBs = bs [ start : end ]
// move start to after the current new line position
start = end + 1
// Write 2 preceding lines + the target line
if targetLineNum - currentLineNum < 3 {
_ , _ = sb . Write ( lineBs )
_ = sb . WriteByte ( '\n' )
}
}
// FIXME: this algorithm could provide incorrect results and mislead the developers.
// For example: Undefined function "file" in template .....
// {{Func .file.Addition file.Deletion .file.Addition}}
// ^^^^ ^(the real error is here)
// The pointer is added to the first one, but the second one is the real incorrect one.
//
// If there is a provided target to look for in the line add a pointer to it
// e.g. ^^^^^^^
if target != "" {
targetPos := bytes . Index ( lineBs , [ ] byte ( target ) )
if targetPos >= 0 {
position = targetPos
}
}
if position >= 0 {
// take the current line and replace preceding text with whitespace (except for tab)
for i := range lineBs [ : position ] {
if lineBs [ i ] != '\t' {
lineBs [ i ] = ' '
}
}
// write the preceding "space"
_ , _ = sb . Write ( lineBs [ : position ] )
// Now write the ^^ pointer
targetLen := len ( target )
if targetLen == 0 {
targetLen = 1
}
_ , _ = sb . WriteString ( strings . Repeat ( "^" , targetLen ) )
_ = sb . WriteByte ( '\n' )
}
// Finally write the footer
sb . WriteString ( dashSeparator )
return sb . String ( )
}