diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-12-04 12:07:54 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2023-12-04 12:07:54 +0100 |
commit | 9f978d387f8b7cb6bc03fe6b4dd52bb16862a784 (patch) | |
tree | dc53e021fbf8a89e7ff0d3e86bbe9416ce5d7ecb /tpl | |
parent | 14d85ec136413dcfc96ad8e4d31633f8f9cbf410 (diff) | |
download | hugo-9f978d387f8b7cb6bc03fe6b4dd52bb16862a784.tar.gz hugo-9f978d387f8b7cb6bc03fe6b4dd52bb16862a784.zip |
Pull in the latest code from Go's template packages (#11771)
Fixes #10707
Fixes #11507
Diffstat (limited to 'tpl')
21 files changed, 413 insertions, 160 deletions
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go index 064b09107..fef952fa6 100644 --- a/tpl/internal/go_templates/fmtsort/sort_test.go +++ b/tpl/internal/go_templates/fmtsort/sort_test.go @@ -6,13 +6,14 @@ package fmtsort_test import ( "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" "math" "reflect" "sort" "strings" "testing" "unsafe" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" ) 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(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(any(any(0))), iFace, 1, 2, 3), + ct(reflect.TypeOf(any(0)), iFace, 1, 2, 3), } var iFace any @@ -190,12 +191,15 @@ func sprintKey(key reflect.Value) string { var ( ints [3]int chans = makeChans() + // pin runtime.Pinner ) func makeChans() []chan int { cs := []chan int{make(chan int), make(chan int), make(chan int)} // 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 { return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer()) }) diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 061f17ddb..3acb9bdf0 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -22,10 +22,15 @@ type context struct { delim delim urlPart urlPart jsCtx jsCtx - attr attr - element element - n parse.Node // for range break/continue - err *Error + // 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 + element element + n parse.Node // for range break/continue + err *Error } func (c context) String() string { @@ -121,8 +126,8 @@ const ( stateJSDqStr // stateJSSqStr occurs inside a JavaScript single quoted string. stateJSSqStr - // stateJSBqStr occurs inside a JavaScript back quoted string. - stateJSBqStr + // stateJSTmplLit occurs inside a JavaScript back quoted string. + stateJSTmplLit // stateJSRegexp occurs inside a JavaScript regexp literal. stateJSRegexp // 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 -// <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. func isInScriptLiteral(s state) bool { // Ignore the comment states (stateJSBlockCmt, stateJSLineCmt, // stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already // omitted from the output. switch s { - case stateJSDqStr, stateJSSqStr, stateJSBqStr, stateJSRegexp: + case stateJSDqStr, stateJSSqStr, stateJSTmplLit, stateJSRegexp: return true } return false diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index a4450a4a9..de4b4de1e 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -222,6 +222,10 @@ const ( // Discussion: // Package html/template does not support actions inside of JS template // 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 ) diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index 5d47a83d3..334bbce0f 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -8,8 +8,6 @@ import ( "bytes" "fmt" "html" - - //"internal/godebug" "io" "regexp" @@ -64,22 +62,23 @@ func evalArgs(args ...any) string { // funcMap maps command names to functions that render their inputs safe. var funcMap = template.FuncMap{ - "_html_template_attrescaper": attrEscaper, - "_html_template_commentescaper": commentEscaper, - "_html_template_cssescaper": cssEscaper, - "_html_template_cssvaluefilter": cssValueFilter, - "_html_template_htmlnamefilter": htmlNameFilter, - "_html_template_htmlescaper": htmlEscaper, - "_html_template_jsregexpescaper": jsRegexpEscaper, - "_html_template_jsstrescaper": jsStrEscaper, - "_html_template_jsvalescaper": jsValEscaper, - "_html_template_nospaceescaper": htmlNospaceEscaper, - "_html_template_rcdataescaper": rcdataEscaper, - "_html_template_srcsetescaper": srcsetFilterAndEscaper, - "_html_template_urlescaper": urlEscaper, - "_html_template_urlfilter": urlFilter, - "_html_template_urlnormalizer": urlNormalizer, - "_eval_args_": evalArgs, + "_html_template_attrescaper": attrEscaper, + "_html_template_commentescaper": commentEscaper, + "_html_template_cssescaper": cssEscaper, + "_html_template_cssvaluefilter": cssValueFilter, + "_html_template_htmlnamefilter": htmlNameFilter, + "_html_template_htmlescaper": htmlEscaper, + "_html_template_jsregexpescaper": jsRegexpEscaper, + "_html_template_jsstrescaper": jsStrEscaper, + "_html_template_jstmpllitescaper": jsTmplLitEscaper, + "_html_template_jsvalescaper": jsValEscaper, + "_html_template_nospaceescaper": htmlNospaceEscaper, + "_html_template_rcdataescaper": rcdataEscaper, + "_html_template_srcsetescaper": srcsetFilterAndEscaper, + "_html_template_urlescaper": urlEscaper, + "_html_template_urlfilter": urlFilter, + "_html_template_urlnormalizer": urlNormalizer, + "_eval_args_": evalArgs, } // escaper collects type inferences about templates and changes needed to make @@ -164,7 +163,6 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } -// Modified by Hugo. // var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") // escapeAction escapes an action template node. @@ -230,16 +228,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { c.jsCtx = jsCtxDivOp case stateJSDqStr, stateJSSqStr: s = append(s, "_html_template_jsstrescaper") - case stateJSBqStr: - if SecurityAllowActionJSTmpl.Load() { - // 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 stateJSTmplLit: + s = append(s, "_html_template_jstmpllitescaper") case stateJSRegexp: s = append(s, "_html_template_jsregexpescaper") case stateCSS: @@ -398,6 +388,9 @@ var redundantFuncs = map[string]map[string]bool{ "_html_template_jsstrescaper": { "_html_template_attrescaper": true, }, + "_html_template_jstmpllitescaper": { + "_html_template_attrescaper": true, + }, "_html_template_urlescaper": { "_html_template_urlnormalizer": true, }, diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index 4ad5316fb..b202afde0 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -35,14 +35,14 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) { func TestEscape(t *testing.T) { data := struct { - F, T bool - C, G, H string - A, E []string - B, M json.Marshaler - N int - U any // untyped nil - Z *int // typed nil - W htmltemplate.HTML + F, T bool + C, G, H, I string + A, E []string + B, M json.Marshaler + N int + U any // untyped nil + Z *int // typed nil + W htmltemplate.HTML }{ F: false, T: true, @@ -57,6 +57,7 @@ func TestEscape(t *testing.T) { U: nil, Z: nil, W: htmltemplate.HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), + I: "${ asd `` }", } pdata := &data @@ -723,6 +724,21 @@ func TestEscape(t *testing.T) { "<p name=\"{{.U}}\">", "<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 { @@ -981,6 +997,31 @@ func TestErrors(t *testing.T) { "<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. { "{{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. `predefined escaper "urlquery" disallowed in template`, }, - { - "<script>var tmpl = `asd {{.}}`;</script>", - `{{.}} appears in a JS template literal`, - }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1354,7 +1391,7 @@ func TestEscapeText(t *testing.T) { }, { "<a onclick=\"`foo", - context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript}, + context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript}, }, { `<A ONCLICK="'`, @@ -1696,6 +1733,94 @@ func TestEscapeText(t *testing.T) { `<svg:a svg:onclick="x()">`, 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 { diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index 8f9b893b4..e91aeafa1 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -325,12 +325,16 @@ var execTests = []execTest{ {"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, - {"nested assignment", + { + "nested assignment", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", - "3", tVal, true}, - {"nested assignment changes the last declaration", + "3", tVal, true, + }, + { + "nested assignment changes the last declaration", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", - "1", tVal, true}, + "1", tVal, true, + }, // Type with String method. {"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: <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 chained var", + { + "method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method", + "true", tVal, true, + }, + { + "chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method on variable", + "true", tVal, true, + }, + { + "chained method on variable", "{{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 nil", "{{call .NilOKFunc nil}}", "true", tVal, 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}, // HTML. - {"html", `{{html "<script>alert(\"XSS\");</script>"}}`, - "<script>alert("XSS");</script>", nil, true}, - {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, - "<script>alert("XSS");</script>", nil, true}, + { + "html", `{{html "<script>alert(\"XSS\");</script>"}}`, + "<script>alert("XSS");</script>", nil, true, + }, + { + "html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, + "<script>alert("XSS");</script>", nil, true, + }, {"html", `{{html .PS}}`, "a string", tVal, true}, {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template @@ -838,7 +852,7 @@ var delimPairs = []string{ func TestDelims(t *testing.T) { const hello = "Hello, world" - var value = struct{ Str string }{hello} + value := struct{ Str string }{hello} for i := 0; i < len(delimPairs); i += 2 { text := ".Str" left := delimPairs[i+0] @@ -861,7 +875,7 @@ func TestDelims(t *testing.T) { if err != nil { 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) if err != nil { t.Fatalf("delim %q exec err %s", left, err) @@ -962,7 +976,7 @@ const treeTemplate = ` ` func TestTree(t *testing.T) { - var tree = &Tree{ + tree := &Tree{ 1, &Tree{ 2, &Tree{ @@ -1213,7 +1227,7 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(strings.Builder) - var cmpStruct = struct { + cmpStruct := struct { Uthree, Ufour uint NegOne, Three int Ptr, NilPtr *int diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go index 98e03ad3c..99edf8f68 100644 --- a/tpl/internal/go_templates/htmltemplate/hugo_template.go +++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go @@ -14,15 +14,9 @@ package template import ( - "sync/atomic" - 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 diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go index 3b5178eb1..79d9102e5 100644 --- a/tpl/internal/go_templates/htmltemplate/js.go +++ b/tpl/internal/go_templates/htmltemplate/js.go @@ -239,6 +239,11 @@ func jsStrEscaper(args ...any) string { return replace(s, jsStrReplacementTable) } +func jsTmplLitEscaper(args ...any) string { + s, _ := stringify(args...) + return replace(s, jsBqStrReplacementTable) +} + // jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression // specials so the result is treated literally when included in a regular // 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 // overencode existing escapes since this table has no entry for `\`. var jsStrNormReplacementTable = []string{ @@ -345,6 +375,7 @@ var jsStrNormReplacementTable = []string{ '<': `\u003c`, '>': `\u003e`, } + var jsRegexpReplacementTable = []string{ 0: `\u0000`, '\t': `\t`, diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go index be7a92051..eed1e8bcc 100644 --- a/tpl/internal/go_templates/htmltemplate/state_string.go +++ b/tpl/internal/go_templates/htmltemplate/state_string.go @@ -21,7 +21,7 @@ func _() { _ = x[stateJS-10] _ = x[stateJSDqStr-11] _ = x[stateJSSqStr-12] - _ = x[stateJSBqStr-13] + _ = x[stateJSTmplLit-13] _ = x[stateJSRegexp-14] _ = x[stateJSBlockCmt-15] _ = x[stateJSLineCmt-16] @@ -39,9 +39,9 @@ func _() { _ = 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 { if i >= state(len(_state_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index 432c365d3..d5a05f66d 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -27,8 +27,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){ stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, - stateJSBqStr: tJSDelimited, stateJSRegexp: tJSDelimited, + stateJSTmplLit: tJSTmpl, stateJSBlockCmt: tBlockCmt, stateJSLineCmt: 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. func tJS(c context, s []byte) (context, int) { - i := bytes.IndexAny(s, "\"`'/<-#") + i := bytes.IndexAny(s, "\"`'/{}<-#") if i == -1 { // Entire input is non string, comment, regexp tokens. c.jsCtx = nextJSCtx(s, c.jsCtx) @@ -283,7 +283,7 @@ func tJS(c context, s []byte) (context, int) { case '\'': c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp case '`': - c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp + c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp case '/': switch { 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] == '!' { 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: panic("unreachable") } 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 // states. func tJSDelimited(c context, s []byte) (context, int) { @@ -333,8 +387,6 @@ func tJSDelimited(c context, s []byte) (context, int) { switch c.state { case stateJSSqStr: specials = `\'` - case stateJSBqStr: - specials = "`\\" case stateJSRegexp: specials = `\/[]` } diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go index 88791b3b4..144602248 100644 --- a/tpl/internal/go_templates/testenv/exec.go +++ b/tpl/internal/go_templates/testenv/exec.go @@ -60,13 +60,6 @@ func tryExec() error { // may as well use the same path so that this branch can be tested without // 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 // no-op flag to check for overall exec support. 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 // variables that could modify the behavior of the Go tools such as // 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 { if cmd.Env != nil { panic("environment already set") } - for _, env := range os.Environ() { + for _, env := range cmd.Environ() { // Exclude GODEBUG from the environment to prevent its output // from breaking tests that are trying to parse other command output. if strings.HasPrefix(env, "GODEBUG=") { diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index 2430ae6cf..97d897a1f 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -15,10 +15,6 @@ import ( "errors" "flag" "fmt" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" - - //"internal/platform" "os" "os/exec" "path/filepath" @@ -27,6 +23,8 @@ import ( "strings" "sync" "testing" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" ) // 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” // and then run them with os.StartProcess or exec.Command. +// Modified by Hugo (not needed) func HasGoBuild() bool { - // Modified by Hugo (not needed) 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 { // For now, having go run and having go build are the same. 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. func MustHaveGoRun(t testing.TB) { if !HasGoRun() { @@ -300,8 +298,8 @@ func MustHaveCGO(t testing.TB) { // CanInternalLink reports whether the current system can link programs with // internal linking. +// Modified by Hugo (not needed) func CanInternalLink(withCgo bool) bool { - // Modified by Hugo (not needed) return false } @@ -320,8 +318,8 @@ func MustInternalLink(t testing.TB, withCgo bool) { // MustHaveBuildMode reports whether the current system can build programs in // the given build mode. // If not, MustHaveBuildMode calls t.Skip with an explanation. +// Modified by Hugo (not needed) func MustHaveBuildMode(t testing.TB, buildmode string) { - // Modified by Hugo (not needed) } // 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) } } diff --git a/tpl/internal/go_templates/testenv/testenv_notunix.go b/tpl/internal/go_templates/testenv/testenv_notunix.go index 9d55f6258..916f18153 100644 --- a/tpl/internal/go_templates/testenv/testenv_notunix.go +++ b/tpl/internal/go_templates/testenv/testenv_notunix.go @@ -7,6 +7,8 @@ package testenv import ( + "errors" + "io/fs" "os" ) @@ -15,6 +17,5 @@ import ( var Sigquit = os.Kill func syscallIsNotSupported(err error) bool { - // Removed by Hugo (not supported in Go 1.20). - return false + return errors.Is(err, fs.ErrPermission) } diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go index 564bff1ab..13e11abfb 100644 --- a/tpl/internal/go_templates/testenv/testenv_test.go +++ b/tpl/internal/go_templates/testenv/testenv_test.go @@ -5,13 +5,13 @@ package testenv_test import ( - "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv" - //"internal/platform" "os" "path/filepath" "runtime" "strings" "testing" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv" ) 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") +} diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go index abb89eeca..296eefc7a 100644 --- a/tpl/internal/go_templates/testenv/testenv_unix.go +++ b/tpl/internal/go_templates/testenv/testenv_unix.go @@ -7,6 +7,8 @@ package testenv import ( + "errors" + "io/fs" "syscall" ) @@ -15,6 +17,27 @@ import ( var Sigquit = syscall.SIGQUIT 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 } diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go index 4c01b05eb..032784bc3 100644 --- a/tpl/internal/go_templates/texttemplate/doc.go +++ b/tpl/internal/go_templates/texttemplate/doc.go @@ -438,13 +438,13 @@ produce the text By construction, a template may reside in only one association. If it's necessary to have a template addressable from multiple associations, the 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; -see the ParseFiles and ParseGlob functions and methods for simple ways to parse -related templates stored in files. +see [ParseFiles], [ParseGlob], [Template.ParseFiles] and [Template.ParseGlob] +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 might write, diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index 597866c68..73153c764 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -7,12 +7,13 @@ package template import ( "errors" "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "io" "reflect" "runtime" "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 diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index b8b3f3bbe..030e20ca0 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -320,12 +320,16 @@ var execTests = []execTest{ {"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, - {"nested assignment", + { + "nested assignment", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", - "3", tVal, true}, - {"nested assignment changes the last declaration", + "3", tVal, true, + }, + { + "nested assignment changes the last declaration", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", - "1", tVal, true}, + "1", tVal, true, + }, // Type with String method. {"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 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 chained var", + { + "method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method", + "true", tVal, true, + }, + { + "chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method on variable", + "true", tVal, true, + }, + { + "chained method on variable", "{{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 nil", "{{call .NilOKFunc nil}}", "true", tVal, 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}, // HTML. - {"html", `{{html "<script>alert(\"XSS\");</script>"}}`, - "<script>alert("XSS");</script>", nil, true}, - {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, - "<script>alert("XSS");</script>", nil, true}, + { + "html", `{{html "<script>alert(\"XSS\");</script>"}}`, + "<script>alert("XSS");</script>", nil, true, + }, + { + "html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, + "<script>alert("XSS");</script>", nil, true, + }, {"html", `{{html .PS}}`, "a string", tVal, true}, {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "<no value>", tVal, true}, @@ -844,7 +858,7 @@ var delimPairs = []string{ func TestDelims(t *testing.T) { const hello = "Hello, world" - var value = struct{ Str string }{hello} + value := struct{ Str string }{hello} for i := 0; i < len(delimPairs); i += 2 { text := ".Str" left := delimPairs[i+0] @@ -867,7 +881,7 @@ func TestDelims(t *testing.T) { if err != nil { 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) if err != nil { t.Fatalf("delim %q exec err %s", left, err) @@ -990,7 +1004,7 @@ const treeTemplate = ` ` func TestTree(t *testing.T) { - var tree = &Tree{ + tree := &Tree{ 1, &Tree{ 2, &Tree{ @@ -1243,7 +1257,7 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(strings.Builder) - var cmpStruct = struct { + cmpStruct := struct { Uthree, Ufour uint NegOne, Three int Ptr, NilPtr *int diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go index b5a8c9ec5..a949f896f 100644 --- a/tpl/internal/go_templates/texttemplate/funcs.go +++ b/tpl/internal/go_templates/texttemplate/funcs.go @@ -478,7 +478,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) { case k1 == uintKind && k2 == intKind: truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int()) default: - if arg1 != zero && arg != zero { + if arg1.IsValid() && arg.IsValid() { return false, errBadComparison } } diff --git a/tpl/template.go b/tpl/template.go index 70fdf2d3d..61aaa993f 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -169,13 +169,6 @@ func SetPageInContext(ctx context.Context, p page) context.Context { 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 { IsNode() bool } diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go index fa511fbab..1b2cffce6 100644 --- a/tpl/tplimpl/integration_test.go +++ b/tpl/tplimpl/integration_test.go @@ -114,7 +114,6 @@ and2: true or2: true counter2: 3 `) - } // Issue 10495 @@ -163,7 +162,6 @@ title: "S3P1" } func TestGoTemplateBugs(t *testing.T) { - t.Run("Issue 11112", func(t *testing.T) { t.Parallel() @@ -188,11 +186,9 @@ func TestGoTemplateBugs(t *testing.T) { b.AssertFileContent("public/index.html", `key = value`) }) - } func TestSecurityAllowActionJSTmpl(t *testing.T) { - filesTemplate := ` -- config.toml -- SECURITYCONFIG @@ -211,20 +207,6 @@ var a = §§{{.Title }}§§; }, ).BuildE() - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal") - - files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", ` -[security] -[security.gotemplates] -allowActionJSTmpl = true -`) - - b = hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).Build() - + // This used to fail, but not in >= Hugo 0.121.0. + b.Assert(err, qt.IsNil) } |