Pull in the latest code from Go's template packages (#11771)

Fixes #10707
Fixes #11507
This commit is contained in:
Bjørn Erik Pedersen 2023-12-04 12:07:54 +01:00 committed by GitHub
parent 14d85ec136
commit 9f978d387f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 417 additions and 190 deletions

View file

@ -34,7 +34,6 @@ import (
hglob "github.com/gohugoio/hugo/hugofs/glob" hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -91,9 +90,6 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
return nil, fmt.Errorf("failed to init config: %w", err) return nil, fmt.Errorf("failed to init config: %w", err)
} }
// This is unfortunate, but these are global settings.
tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl)
loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning) loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning)
return configs, nil return configs, nil

View file

@ -68,9 +68,6 @@ type Config struct {
// Allow inline shortcodes // Allow inline shortcodes
EnableInlineShortcodes bool `json:"enableInlineShortcodes"` EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
// Go templates related security config.
GoTemplates GoTemplates `json:"goTemplates"`
} }
// Exec holds os/exec policies. // Exec holds os/exec policies.
@ -96,15 +93,6 @@ type HTTP struct {
MediaTypes Whitelist `json:"mediaTypes"` MediaTypes Whitelist `json:"mediaTypes"`
} }
type GoTemplates struct {
// Enable to allow template actions inside bakcticks in ES6 template literals.
// This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form
// "... appears in a JS template literal" if you have this in your templates.
// See https://github.com/golang/go/issues/59234
AllowActionJSTmpl bool
}
// ToTOML converts c to TOML with [security] as the root. // ToTOML converts c to TOML with [security] as the root.
func (c Config) ToTOML() string { func (c Config) ToTOML() string {
sec := c.ToSecurityMap() sec := c.ToSecurityMap()
@ -127,7 +115,6 @@ func (c Config) CheckAllowedExec(name string) error {
} }
} }
return nil return nil
} }
func (c Config) CheckAllowedGetEnv(name string) error { func (c Config) CheckAllowedGetEnv(name string) error {
@ -176,7 +163,6 @@ func (c Config) ToSecurityMap() map[string]any {
"security": m, "security": m,
} }
return sec return sec
} }
// DecodeConfig creates a privacy Config from a given Hugo configuration. // DecodeConfig creates a privacy Config from a given Hugo configuration.
@ -206,15 +192,14 @@ func DecodeConfig(cfg config.Provider) (Config, error) {
} }
return sc, nil return sc, nil
} }
func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
return func( return func(
f reflect.Type, f reflect.Type,
t reflect.Type, t reflect.Type,
data any) (any, error) { data any,
) (any, error) {
if t != reflect.TypeOf(Whitelist{}) { if t != reflect.TypeOf(Whitelist{}) {
return data, nil return data, nil
} }
@ -222,7 +207,6 @@ func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
wl := types.ToStringSlicePreserveString(data) wl := types.ToStringSlicePreserveString(data)
return NewWhitelist(wl...) return NewWhitelist(wl...)
} }
} }

View file

@ -53,7 +53,6 @@ getEnv=["a", "b"]
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
c.Assert(pc.Funcs.Getenv.Accept("a"), qt.IsTrue) c.Assert(pc.Funcs.Getenv.Accept("a"), qt.IsTrue)
c.Assert(pc.Funcs.Getenv.Accept("c"), qt.IsFalse) c.Assert(pc.Funcs.Getenv.Accept("c"), qt.IsFalse)
}) })
c.Run("String whitelist", func(c *qt.C) { c.Run("String whitelist", func(c *qt.C) {
@ -80,7 +79,6 @@ osEnv="b"
c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse) c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse)
c.Assert(pc.Exec.OsEnv.Accept("b"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("b"), qt.IsTrue)
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
}) })
c.Run("Default exec.osEnv", func(c *qt.C) { c.Run("Default exec.osEnv", func(c *qt.C) {
@ -105,7 +103,6 @@ allow="a"
c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue) c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue)
c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue)
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
}) })
c.Run("Enable inline shortcodes, legacy", func(c *qt.C) { c.Run("Enable inline shortcodes, legacy", func(c *qt.C) {
@ -129,9 +126,7 @@ osEnv="b"
pc, err := DecodeConfig(cfg) pc, err := DecodeConfig(cfg)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(pc.EnableInlineShortcodes, qt.IsTrue) c.Assert(pc.EnableInlineShortcodes, qt.IsTrue)
}) })
} }
func TestToTOML(t *testing.T) { func TestToTOML(t *testing.T) {
@ -140,7 +135,7 @@ func TestToTOML(t *testing.T) {
got := DefaultConfig.ToTOML() got := DefaultConfig.ToTOML()
c.Assert(got, qt.Equals, c.Assert(got, qt.Equals,
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
) )
} }
@ -169,5 +164,4 @@ func TestDecodeConfigDefault(t *testing.T) {
c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse)
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse)
} }

View file

@ -16,7 +16,7 @@ import (
) )
func main() { func main() {
// The current is built with 2c1e5b05fe39fc5e6c730dd60e82946b8e67c6ba, tag: go1.21.1. // The current is built with 446a5dcf5a3230ce9832682d8f521071d8a34a2b (go 1.22 dev. Thu Oct 5 12:20:11 2023 -0700)
fmt.Println("Forking ...") fmt.Println("Forking ...")
defer fmt.Println("Done ...") defer fmt.Println("Done ...")

View file

@ -6,13 +6,14 @@ package fmtsort_test
import ( import (
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math" "math"
"reflect" "reflect"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"unsafe" "unsafe"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
) )
var compareTests = [][]reflect.Value{ var compareTests = [][]reflect.Value{
@ -38,7 +39,7 @@ var compareTests = [][]reflect.Value{
ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]), ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]),
ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}), ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}),
ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}), ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}),
ct(reflect.TypeOf(any(any(0))), iFace, 1, 2, 3), ct(reflect.TypeOf(any(0)), iFace, 1, 2, 3),
} }
var iFace any var iFace any
@ -190,12 +191,15 @@ func sprintKey(key reflect.Value) string {
var ( var (
ints [3]int ints [3]int
chans = makeChans() chans = makeChans()
// pin runtime.Pinner
) )
func makeChans() []chan int { func makeChans() []chan int {
cs := []chan int{make(chan int), make(chan int), make(chan int)} cs := []chan int{make(chan int), make(chan int), make(chan int)}
// Order channels by address. See issue #49431. // Order channels by address. See issue #49431.
// TODO: pin these pointers once pinning is available (#46787). for i := range cs {
reflect.ValueOf(cs[i]).UnsafePointer()
}
sort.Slice(cs, func(i, j int) bool { sort.Slice(cs, func(i, j int) bool {
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer()) return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
}) })

View file

@ -22,6 +22,11 @@ type context struct {
delim delim delim delim
urlPart urlPart urlPart urlPart
jsCtx jsCtx jsCtx jsCtx
// jsBraceDepth contains the current depth, for each JS template literal
// string interpolation expression, of braces we've seen. This is used to
// determine if the next } will close a JS template literal string
// interpolation expression or not.
jsBraceDepth []int
attr attr attr attr
element element element element
n parse.Node // for range break/continue n parse.Node // for range break/continue
@ -121,8 +126,8 @@ const (
stateJSDqStr stateJSDqStr
// stateJSSqStr occurs inside a JavaScript single quoted string. // stateJSSqStr occurs inside a JavaScript single quoted string.
stateJSSqStr stateJSSqStr
// stateJSBqStr occurs inside a JavaScript back quoted string. // stateJSTmplLit occurs inside a JavaScript back quoted string.
stateJSBqStr stateJSTmplLit
// stateJSRegexp occurs inside a JavaScript regexp literal. // stateJSRegexp occurs inside a JavaScript regexp literal.
stateJSRegexp stateJSRegexp
// stateJSBlockCmt occurs inside a JavaScript /* block comment */. // stateJSBlockCmt occurs inside a JavaScript /* block comment */.
@ -176,14 +181,14 @@ func isInTag(s state) bool {
} }
// isInScriptLiteral returns true if s is one of the literal states within a // isInScriptLiteral returns true if s is one of the literal states within a
// <script> tag, and as such occurances of "<!--", "<script", and "</script" // <script> tag, and as such occurrences of "<!--", "<script", and "</script"
// need to be treated specially. // need to be treated specially.
func isInScriptLiteral(s state) bool { func isInScriptLiteral(s state) bool {
// Ignore the comment states (stateJSBlockCmt, stateJSLineCmt, // Ignore the comment states (stateJSBlockCmt, stateJSLineCmt,
// stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already // stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already
// omitted from the output. // omitted from the output.
switch s { switch s {
case stateJSDqStr, stateJSSqStr, stateJSBqStr, stateJSRegexp: case stateJSDqStr, stateJSSqStr, stateJSTmplLit, stateJSRegexp:
return true return true
} }
return false return false

View file

@ -222,6 +222,10 @@ const (
// Discussion: // Discussion:
// Package html/template does not support actions inside of JS template // Package html/template does not support actions inside of JS template
// literals. // literals.
//
// Deprecated: ErrJSTemplate is no longer returned when an action is present
// in a JS template literal. Actions inside of JS template literals are now
// escaped as expected.
ErrJSTemplate ErrJSTemplate
) )

View file

@ -8,8 +8,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html" "html"
//"internal/godebug"
"io" "io"
"regexp" "regexp"
@ -72,6 +70,7 @@ var funcMap = template.FuncMap{
"_html_template_htmlescaper": htmlEscaper, "_html_template_htmlescaper": htmlEscaper,
"_html_template_jsregexpescaper": jsRegexpEscaper, "_html_template_jsregexpescaper": jsRegexpEscaper,
"_html_template_jsstrescaper": jsStrEscaper, "_html_template_jsstrescaper": jsStrEscaper,
"_html_template_jstmpllitescaper": jsTmplLitEscaper,
"_html_template_jsvalescaper": jsValEscaper, "_html_template_jsvalescaper": jsValEscaper,
"_html_template_nospaceescaper": htmlNospaceEscaper, "_html_template_nospaceescaper": htmlNospaceEscaper,
"_html_template_rcdataescaper": rcdataEscaper, "_html_template_rcdataescaper": rcdataEscaper,
@ -164,7 +163,6 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented") panic("escaping " + n.String() + " is unimplemented")
} }
// Modified by Hugo.
// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") // var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
// escapeAction escapes an action template node. // escapeAction escapes an action template node.
@ -230,16 +228,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
c.jsCtx = jsCtxDivOp c.jsCtx = jsCtxDivOp
case stateJSDqStr, stateJSSqStr: case stateJSDqStr, stateJSSqStr:
s = append(s, "_html_template_jsstrescaper") s = append(s, "_html_template_jsstrescaper")
case stateJSBqStr: case stateJSTmplLit:
if SecurityAllowActionJSTmpl.Load() { s = append(s, "_html_template_jstmpllitescaper")
// debugAllowActionJSTmpl.IncNonDefault()
s = append(s, "_html_template_jsstrescaper")
} else {
return context{
state: stateError,
err: errorf(ErrJSTemplate, n, n.Line, "%s appears in a JS template literal", n),
}
}
case stateJSRegexp: case stateJSRegexp:
s = append(s, "_html_template_jsregexpescaper") s = append(s, "_html_template_jsregexpescaper")
case stateCSS: case stateCSS:
@ -398,6 +388,9 @@ var redundantFuncs = map[string]map[string]bool{
"_html_template_jsstrescaper": { "_html_template_jsstrescaper": {
"_html_template_attrescaper": true, "_html_template_attrescaper": true,
}, },
"_html_template_jstmpllitescaper": {
"_html_template_attrescaper": true,
},
"_html_template_urlescaper": { "_html_template_urlescaper": {
"_html_template_urlnormalizer": true, "_html_template_urlnormalizer": true,
}, },

View file

@ -36,7 +36,7 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
func TestEscape(t *testing.T) { func TestEscape(t *testing.T) {
data := struct { data := struct {
F, T bool F, T bool
C, G, H string C, G, H, I string
A, E []string A, E []string
B, M json.Marshaler B, M json.Marshaler
N int N int
@ -57,6 +57,7 @@ func TestEscape(t *testing.T) {
U: nil, U: nil,
Z: nil, Z: nil,
W: htmltemplate.HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), W: htmltemplate.HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
I: "${ asd `` }",
} }
pdata := &data pdata := &data
@ -723,6 +724,21 @@ func TestEscape(t *testing.T) {
"<p name=\"{{.U}}\">", "<p name=\"{{.U}}\">",
"<p name=\"\">", "<p name=\"\">",
}, },
{
"JS template lit special characters",
"<script>var a = `{{.I}}`</script>",
"<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>",
},
{
"JS template lit special characters, nested lit",
"<script>var a = `${ `{{.I}}` }`</script>",
"<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>",
},
{
"JS template lit, nested JS",
"<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
"<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
},
} }
for _, test := range tests { for _, test := range tests {
@ -981,6 +997,31 @@ func TestErrors(t *testing.T) {
"<script>var a = `${a+b}`</script>`", "<script>var a = `${a+b}`</script>`",
"", "",
}, },
{
"<script>var tmpl = `asd`;</script>",
``,
},
{
"<script>var tmpl = `${1}`;</script>",
``,
},
{
"<script>var tmpl = `${return ``}`;</script>",
``,
},
{
"<script>var tmpl = `${return {{.}} }`;</script>",
``,
},
{
"<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>",
``,
},
{
"<script>var tmpl = `asd ${return \"{\"}`;</script>",
``,
},
// Error cases. // Error cases.
{ {
"{{if .Cond}}<a{{end}}", "{{if .Cond}}<a{{end}}",
@ -1127,10 +1168,6 @@ func TestErrors(t *testing.T) {
// html is allowed since it is the last command in the pipeline, but urlquery is not. // html is allowed since it is the last command in the pipeline, but urlquery is not.
`predefined escaper "urlquery" disallowed in template`, `predefined escaper "urlquery" disallowed in template`,
}, },
{
"<script>var tmpl = `asd {{.}}`;</script>",
`{{.}} appears in a JS template literal`,
},
} }
for _, test := range tests { for _, test := range tests {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
@ -1354,7 +1391,7 @@ func TestEscapeText(t *testing.T) {
}, },
{ {
"<a onclick=\"`foo", "<a onclick=\"`foo",
context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript}, context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript},
}, },
{ {
`<A ONCLICK="'`, `<A ONCLICK="'`,
@ -1696,6 +1733,94 @@ func TestEscapeText(t *testing.T) {
`<svg:a svg:onclick="x()">`, `<svg:a svg:onclick="x()">`,
context{}, context{},
}, },
{
"<script>var a = `",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${",
context{state: stateJS, element: elementScript},
},
{
"<script>var a = `${}",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${`",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${var a = \"",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${var a = \"`",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${var a = \"}",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${``",
context{state: stateJS, element: elementScript},
},
{
"<script>var a = `${`}",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>`${ {} } asd`</script><script>`${ {} }",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var foo = `${ (_ => { return \"x\" })() + \"${",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${ {</script><script>var b = `${ x }",
context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>var foo = `x` + \"${",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>function f() { var a = `${}`; }",
context{state: stateJS, element: elementScript},
},
{
"<script>{`${}`}",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ function f() { return `${1}` }() }`",
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>function f() {`${ function f() { `${1}` } }`}",
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>`${ { `` }",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ { }`",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${",
context{state: stateJS, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ `}",
context{state: stateJSTmplLit, element: elementScript},
},
} }
for _, test := range tests { for _, test := range tests {

View file

@ -325,12 +325,16 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
{"nested assignment", {
"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
"3", tVal, true}, "3", tVal, true,
{"nested assignment changes the last declaration", },
{
"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
"1", tVal, true}, "1", tVal, true,
},
// Type with String method. // Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template {"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
@ -377,15 +381,21 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true}, {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
{"method on chained var", {
"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
{"chained method", },
{
"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
{"chained method on variable", },
{
"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@ -471,10 +481,14 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML. // HTML.
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`, {
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true}, "html", `{{html "<script>alert(\"XSS\");</script>"}}`,
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true}, },
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true}, {"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true}, {"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template {"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template
@ -838,7 +852,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) { func TestDelims(t *testing.T) {
const hello = "Hello, world" const hello = "Hello, world"
var value = struct{ Str string }{hello} value := struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 { for i := 0; i < len(delimPairs); i += 2 {
text := ".Str" text := ".Str"
left := delimPairs[i+0] left := delimPairs[i+0]
@ -861,7 +875,7 @@ func TestDelims(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err) t.Fatalf("delim %q text %q parse err %s", left, text, err)
} }
var b = new(strings.Builder) b := new(strings.Builder)
err = tmpl.Execute(b, value) err = tmpl.Execute(b, value)
if err != nil { if err != nil {
t.Fatalf("delim %q exec err %s", left, err) t.Fatalf("delim %q exec err %s", left, err)
@ -962,7 +976,7 @@ const treeTemplate = `
` `
func TestTree(t *testing.T) { func TestTree(t *testing.T) {
var tree = &Tree{ tree := &Tree{
1, 1,
&Tree{ &Tree{
2, &Tree{ 2, &Tree{
@ -1213,7 +1227,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) { func TestComparison(t *testing.T) {
b := new(strings.Builder) b := new(strings.Builder)
var cmpStruct = struct { cmpStruct := struct {
Uthree, Ufour uint Uthree, Ufour uint
NegOne, Three int NegOne, Three int
Ptr, NilPtr *int Ptr, NilPtr *int

View file

@ -14,15 +14,9 @@
package template package template
import ( import (
"sync/atomic"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
) )
// See https://github.com/golang/go/issues/59234
// Moved here to avoid dependency on Go's internal/debug package.
var SecurityAllowActionJSTmpl atomic.Bool
/* /*
This files contains the Hugo related addons. All the other files in this This files contains the Hugo related addons. All the other files in this

View file

@ -239,6 +239,11 @@ func jsStrEscaper(args ...any) string {
return replace(s, jsStrReplacementTable) return replace(s, jsStrReplacementTable)
} }
func jsTmplLitEscaper(args ...any) string {
s, _ := stringify(args...)
return replace(s, jsBqStrReplacementTable)
}
// jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression // jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression
// specials so the result is treated literally when included in a regular // specials so the result is treated literally when included in a regular
// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by // expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
@ -325,6 +330,31 @@ var jsStrReplacementTable = []string{
'\\': `\\`, '\\': `\\`,
} }
// jsBqStrReplacementTable is like jsStrReplacementTable except it also contains
// the special characters for JS template literals: $, {, and }.
var jsBqStrReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,
'\n': `\n`,
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
'"': `\u0022`,
'`': `\u0060`,
'&': `\u0026`,
'\'': `\u0027`,
'+': `\u002b`,
'/': `\/`,
'<': `\u003c`,
'>': `\u003e`,
'\\': `\\`,
'$': `\u0024`,
'{': `\u007b`,
'}': `\u007d`,
}
// jsStrNormReplacementTable is like jsStrReplacementTable but does not // jsStrNormReplacementTable is like jsStrReplacementTable but does not
// overencode existing escapes since this table has no entry for `\`. // overencode existing escapes since this table has no entry for `\`.
var jsStrNormReplacementTable = []string{ var jsStrNormReplacementTable = []string{
@ -345,6 +375,7 @@ var jsStrNormReplacementTable = []string{
'<': `\u003c`, '<': `\u003c`,
'>': `\u003e`, '>': `\u003e`,
} }
var jsRegexpReplacementTable = []string{ var jsRegexpReplacementTable = []string{
0: `\u0000`, 0: `\u0000`,
'\t': `\t`, '\t': `\t`,

View file

@ -21,7 +21,7 @@ func _() {
_ = x[stateJS-10] _ = x[stateJS-10]
_ = x[stateJSDqStr-11] _ = x[stateJSDqStr-11]
_ = x[stateJSSqStr-12] _ = x[stateJSSqStr-12]
_ = x[stateJSBqStr-13] _ = x[stateJSTmplLit-13]
_ = x[stateJSRegexp-14] _ = x[stateJSRegexp-14]
_ = x[stateJSBlockCmt-15] _ = x[stateJSBlockCmt-15]
_ = x[stateJSLineCmt-16] _ = x[stateJSLineCmt-16]
@ -39,9 +39,9 @@ func _() {
_ = x[stateDead-28] _ = x[stateDead-28]
} }
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead" const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 214, 233, 241, 254, 267, 280, 293, 304, 320, 335, 345, 354} var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 356}
func (i state) String() string { func (i state) String() string {
if i >= state(len(_state_index)-1) { if i >= state(len(_state_index)-1) {

View file

@ -27,8 +27,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){
stateJS: tJS, stateJS: tJS,
stateJSDqStr: tJSDelimited, stateJSDqStr: tJSDelimited,
stateJSSqStr: tJSDelimited, stateJSSqStr: tJSDelimited,
stateJSBqStr: tJSDelimited,
stateJSRegexp: tJSDelimited, stateJSRegexp: tJSDelimited,
stateJSTmplLit: tJSTmpl,
stateJSBlockCmt: tBlockCmt, stateJSBlockCmt: tBlockCmt,
stateJSLineCmt: tLineCmt, stateJSLineCmt: tLineCmt,
stateJSHTMLOpenCmt: tLineCmt, stateJSHTMLOpenCmt: tLineCmt,
@ -270,7 +270,7 @@ func tURL(c context, s []byte) (context, int) {
// tJS is the context transition function for the JS state. // tJS is the context transition function for the JS state.
func tJS(c context, s []byte) (context, int) { func tJS(c context, s []byte) (context, int) {
i := bytes.IndexAny(s, "\"`'/<-#") i := bytes.IndexAny(s, "\"`'/{}<-#")
if i == -1 { if i == -1 {
// Entire input is non string, comment, regexp tokens. // Entire input is non string, comment, regexp tokens.
c.jsCtx = nextJSCtx(s, c.jsCtx) c.jsCtx = nextJSCtx(s, c.jsCtx)
@ -283,7 +283,7 @@ func tJS(c context, s []byte) (context, int) {
case '\'': case '\'':
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
case '`': case '`':
c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp
case '/': case '/':
switch { switch {
case i+1 < len(s) && s[i+1] == '/': case i+1 < len(s) && s[i+1] == '/':
@ -320,12 +320,66 @@ func tJS(c context, s []byte) (context, int) {
if i+1 < len(s) && s[i+1] == '!' { if i+1 < len(s) && s[i+1] == '!' {
c.state, i = stateJSLineCmt, i+1 c.state, i = stateJSLineCmt, i+1
} }
case '{':
// We only care about tracking brace depth if we are inside of a
// template literal.
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
c.jsBraceDepth[len(c.jsBraceDepth)-1]++
case '}':
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
// There are no cases where a brace can be escaped in the JS context
// that are not syntax errors, it seems. Because of this we can just
// count "\}" as "}" and move on, the script is already broken as
// fully fledged parsers will just fail anyway.
c.jsBraceDepth[len(c.jsBraceDepth)-1]--
if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
return c, i + 1
}
c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
c.state = stateJSTmplLit
default: default:
panic("unreachable") panic("unreachable")
} }
return c, i + 1 return c, i + 1
} }
func tJSTmpl(c context, s []byte) (context, int) {
var k int
for {
i := k + bytes.IndexAny(s[k:], "`\\$")
if i < k {
break
}
switch s[i] {
case '\\':
i++
if i == len(s) {
return context{
state: stateError,
err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
}, len(s)
}
case '$':
if len(s) >= i+2 && s[i+1] == '{' {
c.jsBraceDepth = append(c.jsBraceDepth, 0)
c.state = stateJS
return c, i + 2
}
case '`':
// end
c.state = stateJS
return c, i + 1
}
k = i + 1
}
return c, len(s)
}
// tJSDelimited is the context transition function for the JS string and regexp // tJSDelimited is the context transition function for the JS string and regexp
// states. // states.
func tJSDelimited(c context, s []byte) (context, int) { func tJSDelimited(c context, s []byte) (context, int) {
@ -333,8 +387,6 @@ func tJSDelimited(c context, s []byte) (context, int) {
switch c.state { switch c.state {
case stateJSSqStr: case stateJSSqStr:
specials = `\'` specials = `\'`
case stateJSBqStr:
specials = "`\\"
case stateJSRegexp: case stateJSRegexp:
specials = `\/[]` specials = `\/[]`
} }

View file

@ -60,13 +60,6 @@ func tryExec() error {
// may as well use the same path so that this branch can be tested without // may as well use the same path so that this branch can be tested without
// an ios environment. // an ios environment.
/*if !testing.Testing() {
// This isn't a standard 'go test' binary, so we don't know how to
// self-exec in a way that should succeed without side effects.
// Just forget it.
return errors.New("can't probe for exec support with a non-test executable")
}*/
// We know that this is a test executable. We should be able to run it with a // We know that this is a test executable. We should be able to run it with a
// no-op flag to check for overall exec support. // no-op flag to check for overall exec support.
exe, err := os.Executable() exe, err := os.Executable()
@ -99,11 +92,14 @@ func MustHaveExecPath(t testing.TB, path string) {
// CleanCmdEnv will fill cmd.Env with the environment, excluding certain // CleanCmdEnv will fill cmd.Env with the environment, excluding certain
// variables that could modify the behavior of the Go tools such as // variables that could modify the behavior of the Go tools such as
// GODEBUG and GOTRACEBACK. // GODEBUG and GOTRACEBACK.
//
// If the caller wants to set cmd.Dir, set it before calling this function,
// so PWD will be set correctly in the environment.
func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
if cmd.Env != nil { if cmd.Env != nil {
panic("environment already set") panic("environment already set")
} }
for _, env := range os.Environ() { for _, env := range cmd.Environ() {
// Exclude GODEBUG from the environment to prevent its output // Exclude GODEBUG from the environment to prevent its output
// from breaking tests that are trying to parse other command output. // from breaking tests that are trying to parse other command output.
if strings.HasPrefix(env, "GODEBUG=") { if strings.HasPrefix(env, "GODEBUG=") {

View file

@ -15,10 +15,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
//"internal/platform"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -27,6 +23,8 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
) )
// Save the original environment during init for use in checks. A test // Save the original environment during init for use in checks. A test
@ -45,8 +43,8 @@ func Builder() string {
// HasGoBuild reports whether the current system can build programs with “go build” // HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command. // and then run them with os.StartProcess or exec.Command.
// Modified by Hugo (not needed)
func HasGoBuild() bool { func HasGoBuild() bool {
// Modified by Hugo (not needed)
return false return false
} }
@ -69,13 +67,13 @@ func MustHaveGoBuild(t testing.TB) {
} }
} }
// HasGoRun reports whether the current system can run programs with “go run. // HasGoRun reports whether the current system can run programs with “go run.
func HasGoRun() bool { func HasGoRun() bool {
// For now, having go run and having go build are the same. // For now, having go run and having go build are the same.
return HasGoBuild() return HasGoBuild()
} }
// MustHaveGoRun checks that the current system can run programs with “go run. // MustHaveGoRun checks that the current system can run programs with “go run.
// If not, MustHaveGoRun calls t.Skip with an explanation. // If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) { func MustHaveGoRun(t testing.TB) {
if !HasGoRun() { if !HasGoRun() {
@ -300,8 +298,8 @@ func MustHaveCGO(t testing.TB) {
// CanInternalLink reports whether the current system can link programs with // CanInternalLink reports whether the current system can link programs with
// internal linking. // internal linking.
// Modified by Hugo (not needed)
func CanInternalLink(withCgo bool) bool { func CanInternalLink(withCgo bool) bool {
// Modified by Hugo (not needed)
return false return false
} }
@ -320,8 +318,8 @@ func MustInternalLink(t testing.TB, withCgo bool) {
// MustHaveBuildMode reports whether the current system can build programs in // MustHaveBuildMode reports whether the current system can build programs in
// the given build mode. // the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation. // If not, MustHaveBuildMode calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) { func MustHaveBuildMode(t testing.TB, buildmode string) {
// Modified by Hugo (not needed)
} }
// HasSymlink reports whether the current system can use os.Symlink. // HasSymlink reports whether the current system can use os.Symlink.
@ -438,7 +436,7 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
} }
} }
if err := os.WriteFile(dstPath, icfg.Bytes(), 0666); err != nil { if err := os.WriteFile(dstPath, icfg.Bytes(), 0o666); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View file

@ -7,6 +7,8 @@
package testenv package testenv
import ( import (
"errors"
"io/fs"
"os" "os"
) )
@ -15,6 +17,5 @@ import (
var Sigquit = os.Kill var Sigquit = os.Kill
func syscallIsNotSupported(err error) bool { func syscallIsNotSupported(err error) bool {
// Removed by Hugo (not supported in Go 1.20). return errors.Is(err, fs.ErrPermission)
return false
} }

View file

@ -5,13 +5,13 @@
package testenv_test package testenv_test
import ( import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
//"internal/platform"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
) )
func TestGoToolLocation(t *testing.T) { func TestGoToolLocation(t *testing.T) {
@ -83,3 +83,26 @@ func TestMustHaveExec(t *testing.T) {
} }
} }
} }
func TestCleanCmdEnvPWD(t *testing.T) {
// Test that CleanCmdEnv sets PWD if cmd.Dir is set.
switch runtime.GOOS {
case "plan9", "windows":
t.Skipf("PWD is not used on %s", runtime.GOOS)
}
dir := t.TempDir()
cmd := testenv.Command(t, testenv.GoToolPath(t), "help")
cmd.Dir = dir
cmd = testenv.CleanCmdEnv(cmd)
for _, env := range cmd.Env {
if strings.HasPrefix(env, "PWD=") {
pwd := strings.TrimPrefix(env, "PWD=")
if pwd != dir {
t.Errorf("unexpected PWD: want %s, got %s", dir, pwd)
}
return
}
}
t.Error("PWD not set in cmd.Env")
}

View file

@ -7,6 +7,8 @@
package testenv package testenv
import ( import (
"errors"
"io/fs"
"syscall" "syscall"
) )
@ -15,6 +17,27 @@ import (
var Sigquit = syscall.SIGQUIT var Sigquit = syscall.SIGQUIT
func syscallIsNotSupported(err error) bool { func syscallIsNotSupported(err error) bool {
// Removed by Hugo (not supported in Go 1.20) if err == nil {
return false
}
var errno syscall.Errno
if errors.As(err, &errno) {
switch errno {
case syscall.EPERM, syscall.EROFS:
// User lacks permission: either the call requires root permission and the
// user is not root, or the call is denied by a container security policy.
return true
case syscall.EINVAL:
// Some containers return EINVAL instead of EPERM if a system call is
// denied by security policy.
return true
}
}
if errors.Is(err, fs.ErrPermission) {
return true
}
return false return false
} }

View file

@ -438,13 +438,13 @@ produce the text
By construction, a template may reside in only one association. If it's By construction, a template may reside in only one association. If it's
necessary to have a template addressable from multiple associations, the necessary to have a template addressable from multiple associations, the
template definition must be parsed multiple times to create distinct *Template template definition must be parsed multiple times to create distinct *Template
values, or must be copied with the Clone or AddParseTree method. values, or must be copied with [Template.Clone] or [Template.AddParseTree].
Parse may be called multiple times to assemble the various associated templates; Parse may be called multiple times to assemble the various associated templates;
see the ParseFiles and ParseGlob functions and methods for simple ways to parse see [ParseFiles], [ParseGlob], [Template.ParseFiles] and [Template.ParseGlob]
related templates stored in files. for simple ways to parse related templates stored in files.
A template may be executed directly or through ExecuteTemplate, which executes A template may be executed directly or through [Template.ExecuteTemplate], which executes
an associated template identified by name. To invoke our example above, we an associated template identified by name. To invoke our example above, we
might write, might write,

View file

@ -7,12 +7,13 @@ package template
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io" "io"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
) )
// maxExecDepth specifies the maximum stack depth of templates within // maxExecDepth specifies the maximum stack depth of templates within

View file

@ -320,12 +320,16 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
{"nested assignment", {
"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
"3", tVal, true}, "3", tVal, true,
{"nested assignment changes the last declaration", },
{
"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
"1", tVal, true}, "1", tVal, true,
},
// Type with String method. // Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true}, {"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},
@ -372,15 +376,21 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true}, {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
{"method on chained var", {
"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
{"chained method", },
{
"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
{"chained method on variable", },
{
"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true}, "true", tVal, true,
},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@ -466,10 +476,14 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML. // HTML.
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`, {
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true}, "html", `{{html "<script>alert(\"XSS\");</script>"}}`,
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true}, },
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true}, {"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true}, {"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;no value&gt;", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "&lt;no value&gt;", tVal, true},
@ -844,7 +858,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) { func TestDelims(t *testing.T) {
const hello = "Hello, world" const hello = "Hello, world"
var value = struct{ Str string }{hello} value := struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 { for i := 0; i < len(delimPairs); i += 2 {
text := ".Str" text := ".Str"
left := delimPairs[i+0] left := delimPairs[i+0]
@ -867,7 +881,7 @@ func TestDelims(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err) t.Fatalf("delim %q text %q parse err %s", left, text, err)
} }
var b = new(strings.Builder) b := new(strings.Builder)
err = tmpl.Execute(b, value) err = tmpl.Execute(b, value)
if err != nil { if err != nil {
t.Fatalf("delim %q exec err %s", left, err) t.Fatalf("delim %q exec err %s", left, err)
@ -990,7 +1004,7 @@ const treeTemplate = `
` `
func TestTree(t *testing.T) { func TestTree(t *testing.T) {
var tree = &Tree{ tree := &Tree{
1, 1,
&Tree{ &Tree{
2, &Tree{ 2, &Tree{
@ -1243,7 +1257,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) { func TestComparison(t *testing.T) {
b := new(strings.Builder) b := new(strings.Builder)
var cmpStruct = struct { cmpStruct := struct {
Uthree, Ufour uint Uthree, Ufour uint
NegOne, Three int NegOne, Three int
Ptr, NilPtr *int Ptr, NilPtr *int

View file

@ -478,7 +478,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
case k1 == uintKind && k2 == intKind: case k1 == uintKind && k2 == intKind:
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int()) truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default: default:
if arg1 != zero && arg != zero { if arg1.IsValid() && arg.IsValid() {
return false, errBadComparison return false, errBadComparison
} }
} }

View file

@ -169,13 +169,6 @@ func SetPageInContext(ctx context.Context, p page) context.Context {
return context.WithValue(ctx, texttemplate.PageContextKey, p) return context.WithValue(ctx, texttemplate.PageContextKey, p)
} }
// SetSecurityAllowActionJSTmpl sets the global setting for allowing tempalte actions in JS template literals.
// This was added in Hugo 0.114.0.
// See https://github.com/golang/go/issues/59234
func SetSecurityAllowActionJSTmpl(b bool) {
htmltemplate.SecurityAllowActionJSTmpl.Store(b)
}
type page interface { type page interface {
IsNode() bool IsNode() bool
} }

View file

@ -114,7 +114,6 @@ and2: true
or2: true or2: true
counter2: 3 counter2: 3
`) `)
} }
// Issue 10495 // Issue 10495
@ -163,7 +162,6 @@ title: "S3P1"
} }
func TestGoTemplateBugs(t *testing.T) { func TestGoTemplateBugs(t *testing.T) {
t.Run("Issue 11112", func(t *testing.T) { t.Run("Issue 11112", func(t *testing.T) {
t.Parallel() t.Parallel()
@ -188,11 +186,9 @@ func TestGoTemplateBugs(t *testing.T) {
b.AssertFileContent("public/index.html", `key = value`) b.AssertFileContent("public/index.html", `key = value`)
}) })
} }
func TestSecurityAllowActionJSTmpl(t *testing.T) { func TestSecurityAllowActionJSTmpl(t *testing.T) {
filesTemplate := ` filesTemplate := `
-- config.toml -- -- config.toml --
SECURITYCONFIG SECURITYCONFIG
@ -211,20 +207,6 @@ var a = §§{{.Title }}§§;
}, },
).BuildE() ).BuildE()
b.Assert(err, qt.Not(qt.IsNil)) // This used to fail, but not in >= Hugo 0.121.0.
b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal") b.Assert(err, qt.IsNil)
files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", `
[security]
[security.gotemplates]
allowActionJSTmpl = true
`)
b = hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()
} }