2019-07-01 06:07:58 +08:00
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2019-06-19 01:13:12 +08:00
package templates
import (
"bytes"
2021-09-15 23:55:57 +08:00
"errors"
2019-06-19 01:13:12 +08:00
"fmt"
"net/http"
"strconv"
2019-06-22 04:36:26 +08:00
"strings"
2022-05-03 04:55:34 +08:00
"text/template"
2019-06-19 01:13:12 +08:00
2023-11-29 00:39:14 +08:00
"go.uber.org/zap"
2019-07-03 02:37:06 +08:00
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
2019-06-19 01:13:12 +08:00
)
func init ( ) {
2019-08-22 00:46:35 +08:00
caddy . RegisterModule ( Templates { } )
2019-06-19 01:13:12 +08:00
}
2019-12-30 04:16:34 +08:00
// Templates is a middleware which executes response bodies as Go templates.
// The syntax is documented in the Go standard library's
// [text/template package](https://golang.org/pkg/text/template/).
//
2020-04-07 02:50:54 +08:00
// ⚠️ Template functions/actions are still experimental, so they are subject to change.
//
2022-05-03 04:55:34 +08:00
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
//
2019-12-30 04:16:34 +08:00
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
//
2021-05-01 10:17:23 +08:00
// In addition to the standard functions and the Sprig library, Caddy adds
2019-12-30 04:16:34 +08:00
// extra functions and data that are available to a template:
//
2020-04-08 02:29:09 +08:00
// ##### `.Args`
2019-12-30 04:16:34 +08:00
//
2023-10-16 08:58:46 +08:00
// A slice of arguments passed to this page/context, for example
// as the result of a [`include`](#include).
2019-12-30 04:16:34 +08:00
//
// ```
2022-03-04 02:12:37 +08:00
// {{index .Args 0}} // first argument
2019-12-30 04:16:34 +08:00
// ```
//
// ##### `.Cookie`
//
// Gets the value of a cookie by name.
//
// ```
// {{.Cookie "cookiename"}}
// ```
//
2020-04-08 02:29:09 +08:00
// ##### `env`
//
// Gets an environment variable.
//
// ```
// {{env "VAR_NAME"}}
// ```
//
2020-07-21 07:17:38 +08:00
// ##### `placeholder`
//
// Gets an [placeholder variable](/docs/conventions#placeholders).
// The braces (`{}`) have to be omitted.
//
// ```
// {{placeholder "http.request.uri.path"}}
// {{placeholder "http.error.status_code"}}
// ```
//
2024-10-02 20:34:04 +08:00
// As a shortcut, `ph` is an alias for `placeholder`.
//
// ```
// {{ph "http.request.method"}}
// ```
//
2019-12-30 04:16:34 +08:00
// ##### `.Host`
//
// Returns the hostname portion (no port) of the Host header of the HTTP request.
//
// ```
// {{.Host}}
// ```
//
// ##### `httpInclude`
//
2023-05-27 00:16:28 +08:00
// Includes the contents of another file, and renders it in-place,
// by making a virtual HTTP request (also known as a sub-request).
// The URI path must exist on the same virtual server because the
// request does not use sockets; instead, the request is crafted in
// memory and the handler is invoked directly for increased efficiency.
2019-12-30 04:16:34 +08:00
//
// ```
// {{httpInclude "/foo/bar?q=val"}}
// ```
//
2021-09-18 03:00:36 +08:00
// ##### `import`
//
2023-05-27 00:16:28 +08:00
// Reads and returns the contents of another file, and parses it
// as a template, adding any template definitions to the template
// stack. If there are no definitions, the filepath will be the
2023-10-16 08:58:46 +08:00
// definition name. Any `{{ define }}` blocks will be accessible by
// `{{ template }}` or `{{ block }}`. Imports must happen before the
2023-05-27 00:16:28 +08:00
// template or block action is called. Note that the contents are
// NOT escaped, so you should only import trusted template files.
2021-09-18 03:00:36 +08:00
//
// **filename.html**
// ```
// {{ define "main" }}
// content
// {{ end }}
2022-04-28 01:41:37 +08:00
// ```
2021-09-18 03:00:36 +08:00
//
// **index.html**
// ```
2022-05-18 06:56:40 +08:00
// {{ import "/path/to/filename.html" }}
2021-09-18 03:00:36 +08:00
// {{ template "main" }}
// ```
//
2019-12-30 04:16:34 +08:00
// ##### `include`
//
2023-05-27 00:16:28 +08:00
// Includes the contents of another file, rendering it in-place.
// Optionally can pass key-value pairs as arguments to be accessed
2023-10-16 08:58:46 +08:00
// by the included file. Use [`.Args N`](#args) to access the N-th
// argument, 0-indexed. Note that the contents are NOT escaped, so
// you should only include trusted template files.
2019-12-30 04:16:34 +08:00
//
// ```
// {{include "path/to/file.html"}} // no arguments
2023-10-16 08:58:46 +08:00
// {{include "path/to/file.html" "arg0" 1 "value 2"}} // with arguments
2019-12-30 04:16:34 +08:00
// ```
//
2023-05-27 00:16:28 +08:00
// ##### `readFile`
//
// Reads and returns the contents of another file, as-is.
// Note that the contents are NOT escaped, so you should
// only read trusted files.
//
// ```
// {{readFile "path/to/file.html"}}
// ```
//
2019-12-30 04:16:34 +08:00
// ##### `listFiles`
//
2023-10-16 08:58:46 +08:00
// Returns a list of the files in the given directory, which is relative
// to the template context's file root.
2019-12-30 04:16:34 +08:00
//
// ```
// {{listFiles "/mydir"}}
// ```
//
// ##### `markdown`
//
2023-05-27 00:16:28 +08:00
// Renders the given Markdown text as HTML and returns it. This uses the
2022-02-07 13:14:41 +08:00
// [Goldmark](https://github.com/yuin/goldmark) library,
2023-05-27 00:16:28 +08:00
// which is CommonMark compliant. It also has these extensions
2024-05-10 22:08:54 +08:00
// enabled: GitHub Flavored Markdown, Footnote, and syntax
2022-02-07 13:14:41 +08:00
// highlighting provided by [Chroma](https://github.com/alecthomas/chroma).
2019-12-30 04:16:34 +08:00
//
// ```
// {{markdown "My _markdown_ text"}}
// ```
//
// ##### `.RemoteIP`
//
2023-10-16 08:58:46 +08:00
// Returns the connection's IP address.
2019-12-30 04:16:34 +08:00
//
// ```
// {{.RemoteIP}}
// ```
//
2023-10-16 08:58:46 +08:00
// ##### `.ClientIP`
//
// Returns the real client's IP address, if `trusted_proxies` was configured,
// otherwise returns the connection's IP address.
//
// ```
// {{.ClientIP}}
// ```
//
2020-04-25 11:05:09 +08:00
// ##### `.Req`
//
// Accesses the current HTTP request, which has various fields, including:
//
2022-11-15 00:38:02 +08:00
// - `.Method` - the method
// - `.URL` - the URL, which in turn has component fields (Scheme, Host, Path, etc.)
// - `.Header` - the header fields
// - `.Host` - the Host or :authority header of the request
2020-04-25 11:05:09 +08:00
//
// ```
// {{.Req.Header.Get "User-Agent"}}
// ```
//
2022-01-06 04:59:59 +08:00
// ##### `.OriginalReq`
//
2023-10-16 08:58:46 +08:00
// Like [`.Req`](#req), except it accesses the original HTTP
// request before rewrites or other internal modifications.
2022-01-06 04:59:59 +08:00
//
2019-12-30 04:16:34 +08:00
// ##### `.RespHeader.Add`
//
// Adds a header field to the HTTP response.
//
// ```
// {{.RespHeader.Add "Field-Name" "val"}}
// ```
//
// ##### `.RespHeader.Del`
//
// Deletes a header field on the HTTP response.
//
// ```
// {{.RespHeader.Del "Field-Name"}}
// ```
//
// ##### `.RespHeader.Set`
//
// Sets a header field on the HTTP response, replacing any existing value.
//
// ```
// {{.RespHeader.Set "Field-Name" "val"}}
2022-09-02 12:07:52 +08:00
// ```
//
// ##### `httpError`
//
// Returns an error with the given status code to the HTTP handler chain.
//
// ```
// {{if not (fileExists $includedFile)}}{{httpError 404}}{{end}}
2019-12-30 04:16:34 +08:00
// ```
//
// ##### `splitFrontMatter`
//
2023-10-16 08:58:46 +08:00
// Splits front matter out from the body. Front matter is metadata that
// appears at the very beginning of a file or string. Front matter can
// be in YAML, TOML, or JSON formats:
2019-12-30 04:16:34 +08:00
//
// **TOML** front matter starts and ends with `+++`:
//
2023-10-16 08:58:46 +08:00
// ```toml
2019-12-30 04:16:34 +08:00
// +++
// template = "blog"
// title = "Blog Homepage"
// sitename = "A Caddy site"
// +++
// ```
//
// **YAML** is surrounded by `---`:
//
2023-10-16 08:58:46 +08:00
// ```yaml
2019-12-30 04:16:34 +08:00
// ---
// template: blog
// title: Blog Homepage
// sitename: A Caddy site
// ---
// ```
//
// **JSON** is simply `{` and `}`:
//
2023-10-16 08:58:46 +08:00
// ```json
// {
// "template": "blog",
// "title": "Blog Homepage",
// "sitename": "A Caddy site"
// }
2019-12-30 04:16:34 +08:00
// ```
//
// The resulting front matter will be made available like so:
//
// - `.Meta` to access the metadata fields, for example: `{{$parsed.Meta.title}}`
// - `.Body` to access the body after the front matter, for example: `{{markdown $parsed.Body}}`
//
// ##### `stripHTML`
//
// Removes HTML from a string.
//
// ```
// {{stripHTML "Shows <b>only</b> text content"}}
// ```
//
2022-05-25 07:47:08 +08:00
// ##### `humanize`
//
// Transforms size and time inputs to a human readable format.
// This uses the [go-humanize](https://github.com/dustin/go-humanize) library.
//
// The first argument must be a format type, and the last argument
// is the input, or the input can be piped in. The supported format
// types are:
// - **size** which turns an integer amount of bytes into a string like `2.3 MB`
// - **time** which turns a time string into a relative time string like `2 weeks ago`
//
// For the `time` format, the layout for parsing the input can be configured
// by appending a colon `:` followed by the desired time layout. You can
// find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants).
// The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`.
//
2024-05-19 02:55:36 +08:00
// ##### `pathEscape`
//
// Passes a string through `url.PathEscape`, replacing characters that have
// special meaning in URL path parameters (`?`, `&`, `%`).
//
// Useful e.g. to include filenames containing these characters in URL path
// parameters, or use them as an `img` element's `src` attribute.
//
// ```
// {{pathEscape "50%_valid_filename?.jpg"}}
// ```
//
2022-05-25 07:47:08 +08:00
// ```
// {{humanize "size" "2048000"}}
// {{placeholder "http.response.header.Content-Length" | humanize "size"}}
// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}
// {{humanize "time:2006-Jan-02" "2022-May-05"}}
// ```
2019-06-19 01:13:12 +08:00
type Templates struct {
2019-12-24 03:56:41 +08:00
// The root path from which to load files. Required if template functions
// accessing the file system are used (such as include). Default is
// `{http.vars.root}` if set, or current working directory otherwise.
FileRoot string ` json:"file_root,omitempty" `
// The MIME types for which to render templates. It is important to use
// this if the route matchers do not exclude images or other binary files.
// Default is text/plain, text/markdown, and text/html.
MIMETypes [ ] string ` json:"mime_types,omitempty" `
2021-09-25 08:31:01 +08:00
// The template action delimiters. If set, must be precisely two elements:
// the opening and closing delimiters. Default: `["{{", "}}"]`
2019-12-24 03:56:41 +08:00
Delimiters [ ] string ` json:"delimiters,omitempty" `
2022-05-03 04:55:34 +08:00
2023-11-29 00:39:14 +08:00
// Extensions adds functions to the template's func map. These often
// act as components on web pages, for example.
ExtensionsRaw caddy . ModuleMap ` json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions" `
2022-05-03 04:55:34 +08:00
customFuncs [ ] template . FuncMap
2023-11-29 00:39:14 +08:00
logger * zap . Logger
2022-05-03 04:55:34 +08:00
}
2024-03-06 21:53:03 +08:00
// CustomFunctions is the interface for registering custom template functions.
2022-05-03 04:55:34 +08:00
type CustomFunctions interface {
// CustomTemplateFunctions should return the mapping from custom function names to implementations.
CustomTemplateFunctions ( ) template . FuncMap
2019-06-19 01:13:12 +08:00
}
2019-08-22 00:46:35 +08:00
// CaddyModule returns the Caddy module information.
func ( Templates ) CaddyModule ( ) caddy . ModuleInfo {
return caddy . ModuleInfo {
2019-12-11 04:36:46 +08:00
ID : "http.handlers.templates" ,
New : func ( ) caddy . Module { return new ( Templates ) } ,
2019-08-22 00:46:35 +08:00
}
}
2019-06-28 03:09:10 +08:00
// Provision provisions t.
func ( t * Templates ) Provision ( ctx caddy . Context ) error {
2023-11-29 00:39:14 +08:00
t . logger = ctx . Logger ( )
mods , err := ctx . LoadModule ( t , "ExtensionsRaw" )
if err != nil {
return fmt . Errorf ( "loading template extensions: %v" , err )
}
for _ , modIface := range mods . ( map [ string ] any ) {
t . customFuncs = append ( t . customFuncs , modIface . ( CustomFunctions ) . CustomTemplateFunctions ( ) )
2022-05-03 04:55:34 +08:00
}
2019-06-28 03:09:10 +08:00
if t . MIMETypes == nil {
t . MIMETypes = defaultMIMETypes
}
2019-12-24 03:56:41 +08:00
if t . FileRoot == "" {
t . FileRoot = "{http.vars.root}"
2019-09-07 02:36:45 +08:00
}
2019-06-28 03:09:10 +08:00
return nil
}
2019-06-19 01:13:12 +08:00
// Validate ensures t has a valid configuration.
func ( t * Templates ) Validate ( ) error {
if len ( t . Delimiters ) != 0 && len ( t . Delimiters ) != 2 {
return fmt . Errorf ( "delimiters must consist of exactly two elements: opening and closing" )
}
return nil
}
func ( t * Templates ) ServeHTTP ( w http . ResponseWriter , r * http . Request , next caddyhttp . Handler ) error {
buf := bufPool . Get ( ) . ( * bytes . Buffer )
buf . Reset ( )
defer bufPool . Put ( buf )
2019-06-28 03:09:10 +08:00
// shouldBuf determines whether to execute templates on this response,
// since generally we will not want to execute for images or CSS, etc.
2019-10-16 04:07:10 +08:00
shouldBuf := func ( status int , header http . Header ) bool {
ct := header . Get ( "Content-Type" )
2019-06-28 03:09:10 +08:00
for _ , mt := range t . MIMETypes {
if strings . Contains ( ct , mt ) {
return true
}
}
return false
2019-06-22 04:36:26 +08:00
}
rec := caddyhttp . NewResponseRecorder ( w , buf , shouldBuf )
2019-06-19 01:13:12 +08:00
2019-06-22 04:36:26 +08:00
err := next . ServeHTTP ( rec , r )
2019-06-19 01:13:12 +08:00
if err != nil {
return err
}
2019-06-22 04:36:26 +08:00
if ! rec . Buffered ( ) {
return nil
}
2019-06-19 01:13:12 +08:00
2019-06-22 04:36:26 +08:00
err = t . executeTemplate ( rec , r )
2019-06-19 01:13:12 +08:00
if err != nil {
return err
}
2019-10-16 04:07:10 +08:00
rec . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( buf . Len ( ) ) )
rec . Header ( ) . Del ( "Accept-Ranges" ) // we don't know ranges for dynamically-created content
rec . Header ( ) . Del ( "Last-Modified" ) // useless for dynamic content since it's always changing
2019-06-19 01:13:12 +08:00
2020-02-28 10:30:48 +08:00
// we don't know a way to quickly generate etag for dynamic content,
2019-12-24 03:56:41 +08:00
// and weak etags still cause browsers to rely on it even after a
// refresh, so disable them until we find a better way to do this
rec . Header ( ) . Del ( "Etag" )
2019-06-28 03:09:10 +08:00
2020-01-04 02:33:22 +08:00
return rec . WriteResponse ( )
2019-06-19 01:13:12 +08:00
}
2019-06-21 11:49:45 +08:00
// executeTemplate executes the template contained in wb.buf and replaces it with the results.
func ( t * Templates ) executeTemplate ( rr caddyhttp . ResponseRecorder , r * http . Request ) error {
2019-06-19 01:13:12 +08:00
var fs http . FileSystem
2019-12-24 03:56:41 +08:00
if t . FileRoot != "" {
2019-12-30 04:12:52 +08:00
repl := r . Context ( ) . Value ( caddy . ReplacerCtxKey ) . ( * caddy . Replacer )
2019-12-24 03:56:41 +08:00
fs = http . Dir ( repl . ReplaceAll ( t . FileRoot , "." ) )
2019-06-19 01:13:12 +08:00
}
2019-06-19 05:17:48 +08:00
2021-05-01 10:17:23 +08:00
ctx := & TemplateContext {
2022-05-03 04:55:34 +08:00
Root : fs ,
Req : r ,
RespHeader : WrappedHeader { rr . Header ( ) } ,
config : t ,
CustomFuncs : t . customFuncs ,
2019-06-19 01:13:12 +08:00
}
2019-06-21 11:49:45 +08:00
err := ctx . executeTemplateInBuffer ( r . URL . Path , rr . Buffer ( ) )
2019-06-19 01:13:12 +08:00
if err != nil {
2021-09-15 23:55:57 +08:00
// templates may return a custom HTTP error to be propagated to the client,
// otherwise for any other error we assume the template is broken
var handlerErr caddyhttp . HandlerError
if errors . As ( err , & handlerErr ) {
return handlerErr
}
2019-06-19 01:13:12 +08:00
return caddyhttp . Error ( http . StatusInternalServerError , err )
}
return nil
}
2019-06-21 11:49:45 +08:00
// virtualResponseWriter is used in virtualized HTTP requests
// that templates may execute.
2019-06-19 01:13:12 +08:00
type virtualResponseWriter struct {
status int
header http . Header
body * bytes . Buffer
}
func ( vrw * virtualResponseWriter ) Header ( ) http . Header {
return vrw . header
}
func ( vrw * virtualResponseWriter ) WriteHeader ( statusCode int ) {
vrw . status = statusCode
}
func ( vrw * virtualResponseWriter ) Write ( data [ ] byte ) ( int , error ) {
return vrw . body . Write ( data )
}
2019-06-28 03:09:10 +08:00
var defaultMIMETypes = [ ] string {
"text/html" ,
"text/plain" ,
"text/markdown" ,
}
2019-06-19 01:13:12 +08:00
// Interface guards
var (
2019-06-28 03:09:10 +08:00
_ caddy . Provisioner = ( * Templates ) ( nil )
2019-06-19 01:13:12 +08:00
_ caddy . Validator = ( * Templates ) ( nil )
_ caddyhttp . MiddlewareHandler = ( * Templates ) ( nil )
)