code.gitea.io/gitea@v1.19.3/modules/templates/htmlrenderer.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package templates
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/modules/log"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	"code.gitea.io/gitea/modules/watcher"
    17  
    18  	"github.com/unrolled/render"
    19  )
    20  
    21  var (
    22  	rendererKey interface{} = "templatesHtmlRenderer"
    23  
    24  	templateError    = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
    25  	notDefinedError  = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
    26  	unexpectedError  = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
    27  	expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
    28  )
    29  
    30  // HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
    31  func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
    32  	rendererInterface := ctx.Value(rendererKey)
    33  	if rendererInterface != nil {
    34  		renderer, ok := rendererInterface.(*render.Render)
    35  		if ok {
    36  			return ctx, renderer
    37  		}
    38  	}
    39  
    40  	rendererType := "static"
    41  	if !setting.IsProd {
    42  		rendererType = "auto-reloading"
    43  	}
    44  	log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
    45  
    46  	compilingTemplates := true
    47  	defer func() {
    48  		if !compilingTemplates {
    49  			return
    50  		}
    51  
    52  		panicked := recover()
    53  		if panicked == nil {
    54  			return
    55  		}
    56  
    57  		// OK try to handle the panic...
    58  		err, ok := panicked.(error)
    59  		if ok {
    60  			handlePanicError(err)
    61  		}
    62  		log.Fatal("PANIC: Unable to compile templates!\n%v\n\nStacktrace:\n%s", panicked, log.Stack(2))
    63  	}()
    64  
    65  	renderer := render.New(render.Options{
    66  		Extensions:                []string{".tmpl"},
    67  		Directory:                 "templates",
    68  		Funcs:                     NewFuncMap(),
    69  		Asset:                     GetAsset,
    70  		AssetNames:                GetTemplateAssetNames,
    71  		UseMutexLock:              !setting.IsProd,
    72  		IsDevelopment:             false,
    73  		DisableHTTPErrorRendering: true,
    74  	})
    75  	compilingTemplates = false
    76  	if !setting.IsProd {
    77  		watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
    78  			PathsCallback: walkTemplateFiles,
    79  			BetweenCallback: func() {
    80  				defer func() {
    81  					if err := recover(); err != nil {
    82  						log.Error("PANIC: %v\n%s", err, log.Stack(2))
    83  					}
    84  				}()
    85  				renderer.CompileTemplates()
    86  			},
    87  		})
    88  	}
    89  	return context.WithValue(ctx, rendererKey, renderer), renderer
    90  }
    91  
    92  func handlePanicError(err error) {
    93  	wrapFatal(handleNotDefinedPanicError(err))
    94  	wrapFatal(handleUnexpected(err))
    95  	wrapFatal(handleExpectedEnd(err))
    96  	wrapFatal(handleGenericTemplateError(err))
    97  }
    98  
    99  func wrapFatal(format string, args []interface{}) {
   100  	if format == "" {
   101  		return
   102  	}
   103  	log.FatalWithSkip(1, format, args...)
   104  }
   105  
   106  func handleGenericTemplateError(err error) (string, []interface{}) {
   107  	groups := templateError.FindStringSubmatch(err.Error())
   108  	if len(groups) != 4 {
   109  		return "", nil
   110  	}
   111  
   112  	templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
   113  
   114  	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
   115  	if assetErr != nil {
   116  		return "", nil
   117  	}
   118  
   119  	lineNumber, _ := strconv.Atoi(lineNumberStr)
   120  
   121  	line := GetLineFromTemplate(templateName, lineNumber, "", -1)
   122  
   123  	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)}
   124  }
   125  
   126  func handleNotDefinedPanicError(err error) (string, []interface{}) {
   127  	groups := notDefinedError.FindStringSubmatch(err.Error())
   128  	if len(groups) != 4 {
   129  		return "", nil
   130  	}
   131  
   132  	templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
   133  
   134  	functionName, _ = strconv.Unquote(`"` + functionName + `"`)
   135  
   136  	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
   137  	if assetErr != nil {
   138  		return "", nil
   139  	}
   140  
   141  	lineNumber, _ := strconv.Atoi(lineNumberStr)
   142  
   143  	line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
   144  
   145  	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)}
   146  }
   147  
   148  func handleUnexpected(err error) (string, []interface{}) {
   149  	groups := unexpectedError.FindStringSubmatch(err.Error())
   150  	if len(groups) != 4 {
   151  		return "", nil
   152  	}
   153  
   154  	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
   155  	unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
   156  
   157  	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
   158  	if assetErr != nil {
   159  		return "", nil
   160  	}
   161  
   162  	lineNumber, _ := strconv.Atoi(lineNumberStr)
   163  
   164  	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
   165  
   166  	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)}
   167  }
   168  
   169  func handleExpectedEnd(err error) (string, []interface{}) {
   170  	groups := expectedEndError.FindStringSubmatch(err.Error())
   171  	if len(groups) != 4 {
   172  		return "", nil
   173  	}
   174  
   175  	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
   176  
   177  	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
   178  	if assetErr != nil {
   179  		return "", nil
   180  	}
   181  
   182  	lineNumber, _ := strconv.Atoi(lineNumberStr)
   183  
   184  	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
   185  
   186  	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)}
   187  }
   188  
   189  const dashSeparator = "----------------------------------------------------------------------\n"
   190  
   191  // GetLineFromTemplate returns a line from a template with some context
   192  func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
   193  	bs, err := GetAsset("templates/" + templateName + ".tmpl")
   194  	if err != nil {
   195  		return fmt.Sprintf("(unable to read template file: %v)", err)
   196  	}
   197  
   198  	sb := &strings.Builder{}
   199  
   200  	// Write the header
   201  	sb.WriteString(dashSeparator)
   202  
   203  	var lineBs []byte
   204  
   205  	// Iterate through the lines from the asset file to find the target line
   206  	for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
   207  		// Find the next new line
   208  		end := bytes.IndexByte(bs[start:], '\n')
   209  
   210  		// adjust the end to be a direct pointer in to []byte
   211  		if end < 0 {
   212  			end = len(bs)
   213  		} else {
   214  			end += start
   215  		}
   216  
   217  		// set lineBs to the current line []byte
   218  		lineBs = bs[start:end]
   219  
   220  		// move start to after the current new line position
   221  		start = end + 1
   222  
   223  		// Write 2 preceding lines + the target line
   224  		if targetLineNum-currentLineNum < 3 {
   225  			_, _ = sb.Write(lineBs)
   226  			_ = sb.WriteByte('\n')
   227  		}
   228  	}
   229  
   230  	// If there is a provided target to look for in the line add a pointer to it
   231  	// e.g.                                                        ^^^^^^^
   232  	if target != "" {
   233  		targetPos := bytes.Index(lineBs, []byte(target))
   234  		if targetPos >= 0 {
   235  			position = targetPos
   236  		}
   237  	}
   238  	if position >= 0 {
   239  		// take the current line and replace preceding text with whitespace (except for tab)
   240  		for i := range lineBs[:position] {
   241  			if lineBs[i] != '\t' {
   242  				lineBs[i] = ' '
   243  			}
   244  		}
   245  
   246  		// write the preceding "space"
   247  		_, _ = sb.Write(lineBs[:position])
   248  
   249  		// Now write the ^^ pointer
   250  		targetLen := len(target)
   251  		if targetLen == 0 {
   252  			targetLen = 1
   253  		}
   254  		_, _ = sb.WriteString(strings.Repeat("^", targetLen))
   255  		_ = sb.WriteByte('\n')
   256  	}
   257  
   258  	// Finally write the footer
   259  	sb.WriteString(dashSeparator)
   260  
   261  	return sb.String()
   262  }