diff options
Diffstat (limited to 'libgo/go/html')
-rw-r--r-- | libgo/go/html/template/attr.go | 3 | ||||
-rw-r--r-- | libgo/go/html/template/doc.go | 4 | ||||
-rw-r--r-- | libgo/go/html/template/error.go | 28 | ||||
-rw-r--r-- | libgo/go/html/template/escape.go | 240 | ||||
-rw-r--r-- | libgo/go/html/template/escape_test.go | 192 | ||||
-rw-r--r-- | libgo/go/html/template/js.go | 2 | ||||
-rw-r--r-- | libgo/go/html/template/template.go | 20 | ||||
-rw-r--r-- | libgo/go/html/template/transition.go | 4 |
8 files changed, 354 insertions, 139 deletions
diff --git a/libgo/go/html/template/attr.go b/libgo/go/html/template/attr.go index d65d340073d..7438f51f6a9 100644 --- a/libgo/go/html/template/attr.go +++ b/libgo/go/html/template/attr.go @@ -135,9 +135,8 @@ var attrTypeMap = map[string]contentType{ } // attrType returns a conservative (upper-bound on authority) guess at the -// type of the named attribute. +// type of the lowercase named attribute. func attrType(name string) contentType { - name = strings.ToLower(name) if strings.HasPrefix(name, "data-") { // Strip data- so that custom attribute heuristics below are // widely applied. diff --git a/libgo/go/html/template/doc.go b/libgo/go/html/template/doc.go index cb898127432..35d171c3fca 100644 --- a/libgo/go/html/template/doc.go +++ b/libgo/go/html/template/doc.go @@ -65,8 +65,10 @@ functions to each simple action pipeline, so given the excerpt At parse time each {{.}} is overwritten to add escaping functions as necessary. In this case it becomes - <a href="/search?q={{. | urlquery}}">{{. | html}}</a> + <a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a> +where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping +functions. Errors diff --git a/libgo/go/html/template/error.go b/libgo/go/html/template/error.go index cbcaf92e4ab..0e527063ea6 100644 --- a/libgo/go/html/template/error.go +++ b/libgo/go/html/template/error.go @@ -183,6 +183,34 @@ const ( // Look for missing semicolons inside branches, and maybe add // parentheses to make it clear which interpretation you intend. ErrSlashAmbig + + // ErrPredefinedEscaper: "predefined escaper ... disallowed in template" + // Example: + // <div class={{. | html}}>Hello<div> + // Discussion: + // Package html/template already contextually escapes all pipelines to + // produce HTML output safe against code injection. Manually escaping + // pipeline output using the predefined escapers "html" or "urlquery" is + // unnecessary, and may affect the correctness or safety of the escaped + // pipeline output in Go 1.8 and earlier. + // + // In most cases, such as the given example, this error can be resolved by + // simply removing the predefined escaper from the pipeline and letting the + // contextual autoescaper handle the escaping of the pipeline. In other + // instances, where the predefined escaper occurs in the middle of a + // pipeline where subsequent commands expect escaped input, e.g. + // {{.X | html | makeALink}} + // where makeALink does + // return `<a href="`+input+`">link</a>` + // consider refactoring the surrounding template to make use of the + // contextual autoescaper, i.e. + // <a href="{{.X}}">link</a> + // + // To ease migration to Go 1.9 and beyond, "html" and "urlquery" will + // continue to be allowed as the last command in a pipeline. However, if the + // pipeline occurs in an unquoted attribute value context, "html" is + // disallowed. Avoid using "html" and "urlquery" entirely in new templates. + ErrPredefinedEscaper ) func (e *Error) Error() string { diff --git a/libgo/go/html/template/escape.go b/libgo/go/html/template/escape.go index 0e7d2be143b..b51a37039bd 100644 --- a/libgo/go/html/template/escape.go +++ b/libgo/go/html/template/escape.go @@ -19,8 +19,7 @@ import ( // been modified. Otherwise the named templates have been rendered // unusable. func escapeTemplate(tmpl *Template, node parse.Node, name string) error { - e := newEscaper(tmpl) - c, _ := e.escapeTree(context{}, node, name, 0) + c, _ := tmpl.esc.escapeTree(context{}, node, name, 0) var err error if c.err != nil { err, c.err.Name = c.err, name @@ -36,7 +35,7 @@ func escapeTemplate(tmpl *Template, node parse.Node, name string) error { } return err } - e.commit() + tmpl.esc.commit() if t := tmpl.set[name]; t != nil { t.escapeErr = escapeOK t.Tree = t.text.Tree @@ -44,6 +43,21 @@ func escapeTemplate(tmpl *Template, node parse.Node, name string) error { return nil } +// evalArgs formats the list of arguments into a string. It is equivalent to +// fmt.Sprint(args...), except that it deferences all pointers. +func evalArgs(args ...interface{}) string { + // Optimization for simple common case of a single string argument. + if len(args) == 1 { + if s, ok := args[0].(string); ok { + return s + } + } + for i, arg := range args { + args[i] = indirectToStringerOrError(arg) + } + return fmt.Sprint(args...) +} + // funcMap maps command names to functions that render their inputs safe. var funcMap = template.FuncMap{ "_html_template_attrescaper": attrEscaper, @@ -60,22 +74,14 @@ var funcMap = template.FuncMap{ "_html_template_urlescaper": urlEscaper, "_html_template_urlfilter": urlFilter, "_html_template_urlnormalizer": urlNormalizer, -} - -// equivEscapers matches contextual escapers to equivalent template builtins. -var equivEscapers = map[string]string{ - "_html_template_attrescaper": "html", - "_html_template_htmlescaper": "html", - "_html_template_nospaceescaper": "html", - "_html_template_rcdataescaper": "html", - "_html_template_urlescaper": "urlquery", - "_html_template_urlnormalizer": "urlquery", + "_eval_args_": evalArgs, } // escaper collects type inferences about templates and changes needed to make // templates injection safe. type escaper struct { - tmpl *Template + // ns is the nameSpace that this escaper is associated with. + ns *nameSpace // output[templateName] is the output context for a templateName that // has been mangled to include its input context. output map[string]context @@ -92,10 +98,10 @@ type escaper struct { textNodeEdits map[*parse.TextNode][]byte } -// newEscaper creates a blank escaper for the given set. -func newEscaper(t *Template) *escaper { - return &escaper{ - t, +// makeEscaper creates a blank escaper for the given set. +func makeEscaper(n *nameSpace) escaper { + return escaper{ + n, map[string]context{}, map[string]*template.Template{}, map[string]bool{}, @@ -140,6 +146,30 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { return c } c = nudge(c) + // Check for disallowed use of predefined escapers in the pipeline. + for pos, idNode := range n.Pipe.Cmds { + node, ok := idNode.Args[0].(*parse.IdentifierNode) + if !ok { + // A predefined escaper "esc" will never be found as an identifier in a + // Chain or Field node, since: + // - "esc.x ..." is invalid, since predefined escapers return strings, and + // strings do not have methods, keys or fields. + // - "... .esc" is invalid, since predefined escapers are global functions, + // not methods or fields of any types. + // Therefore, it is safe to ignore these two node types. + continue + } + ident := node.Ident + if _, ok := predefinedEscapers[ident]; ok { + if pos < len(n.Pipe.Cmds)-1 || + c.state == stateAttr && c.delim == delimSpaceOrTagEnd && ident == "html" { + return context{ + state: stateError, + err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident), + } + } + } + } s := make([]string, 0, 3) switch c.state { case stateError: @@ -204,75 +234,98 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { return c } -// allIdents returns the names of the identifiers under the Ident field of the node, -// which might be a singleton (Identifier) or a slice (Field or Chain). -func allIdents(node parse.Node) []string { - switch node := node.(type) { - case *parse.IdentifierNode: - return []string{node.Ident} - case *parse.FieldNode: - return node.Ident - case *parse.ChainNode: - return node.Field - } - return nil -} - -// ensurePipelineContains ensures that the pipeline has commands with -// the identifiers in s in order. -// If the pipeline already has some of the sanitizers, do not interfere. -// For example, if p is (.X | html) and s is ["escapeJSVal", "html"] then it -// has one matching, "html", and one to insert, "escapeJSVal", to produce -// (.X | escapeJSVal | html). +// ensurePipelineContains ensures that the pipeline ends with the commands with +// the identifiers in s in order. If the pipeline ends with a predefined escaper +// (i.e. "html" or "urlquery"), merge it with the identifiers in s. func ensurePipelineContains(p *parse.PipeNode, s []string) { if len(s) == 0 { + // Do not rewrite pipeline if we have no escapers to insert. return } - n := len(p.Cmds) - // Find the identifiers at the end of the command chain. - idents := p.Cmds - for i := n - 1; i >= 0; i-- { - if cmd := p.Cmds[i]; len(cmd.Args) != 0 { - if _, ok := cmd.Args[0].(*parse.IdentifierNode); ok { - continue - } - } - idents = p.Cmds[i+1:] - } - dups := 0 - for _, idNode := range idents { - for _, ident := range allIdents(idNode.Args[0]) { - if escFnsEq(s[dups], ident) { - dups++ - if dups == len(s) { - return + // Precondition: p.Cmds contains at most one predefined escaper and the + // escaper will be present at p.Cmds[len(p.Cmds)-1]. This precondition is + // always true because of the checks in escapeAction. + pipelineLen := len(p.Cmds) + if pipelineLen > 0 { + lastCmd := p.Cmds[pipelineLen-1] + if idNode, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok { + if esc := idNode.Ident; predefinedEscapers[esc] { + // Pipeline ends with a predefined escaper. + if len(p.Cmds) == 1 && len(lastCmd.Args) > 1 { + // Special case: pipeline is of the form {{ esc arg1 arg2 ... argN }}, + // where esc is the predefined escaper, and arg1...argN are its arguments. + // Convert this into the equivalent form + // {{ _eval_args_ arg1 arg2 ... argN | esc }}, so that esc can be easily + // merged with the escapers in s. + lastCmd.Args[0] = parse.NewIdentifier("_eval_args_").SetTree(nil).SetPos(lastCmd.Args[0].Position()) + p.Cmds = appendCmd(p.Cmds, newIdentCmd(esc, p.Position())) + pipelineLen++ } - } - } - } - newCmds := make([]*parse.CommandNode, n-len(idents), n+len(s)-dups) - copy(newCmds, p.Cmds) - // Merge existing identifier commands with the sanitizers needed. - for _, idNode := range idents { - pos := idNode.Args[0].Position() - for _, ident := range allIdents(idNode.Args[0]) { - i := indexOfStr(ident, s, escFnsEq) - if i != -1 { - for _, name := range s[:i] { - newCmds = appendCmd(newCmds, newIdentCmd(name, pos)) + // If any of the commands in s that we are about to insert is equivalent + // to the predefined escaper, use the predefined escaper instead. + dup := false + for i, escaper := range s { + if escFnsEq(esc, escaper) { + s[i] = idNode.Ident + dup = true + } + } + if dup { + // The predefined escaper will already be inserted along with the + // escapers in s, so do not copy it to the rewritten pipeline. + pipelineLen-- } - s = s[i+1:] } } - newCmds = appendCmd(newCmds, idNode) } - // Create any remaining sanitizers. + // Rewrite the pipeline, creating the escapers in s at the end of the pipeline. + newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s)) + copy(newCmds, p.Cmds) for _, name := range s { newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position())) } p.Cmds = newCmds } +// predefinedEscapers contains template predefined escapers that are equivalent +// to some contextual escapers. Keep in sync with equivEscapers. +var predefinedEscapers = map[string]bool{ + "html": true, + "urlquery": true, +} + +// equivEscapers matches contextual escapers to equivalent predefined +// template escapers. +var equivEscapers = map[string]string{ + // The following pairs of HTML escapers provide equivalent security + // guarantees, since they all escape '\000', '\'', '"', '&', '<', and '>'. + "_html_template_attrescaper": "html", + "_html_template_htmlescaper": "html", + "_html_template_rcdataescaper": "html", + // These two URL escapers produce URLs safe for embedding in a URL query by + // percent-encoding all the reserved characters specified in RFC 3986 Section + // 2.2 + "_html_template_urlescaper": "urlquery", + // These two functions are not actually equivalent; urlquery is stricter as it + // escapes reserved characters (e.g. '#'), while _html_template_urlnormalizer + // does not. It is therefore only safe to replace _html_template_urlnormalizer + // with urlquery (this happens in ensurePipelineContains), but not the otherI've + // way around. We keep this entry around to preserve the behavior of templates + // written before Go 1.9, which might depend on this substitution taking place. + "_html_template_urlnormalizer": "urlquery", +} + +// escFnsEq reports whether the two escaping functions are equivalent. +func escFnsEq(a, b string) bool { + if e := equivEscapers[a]; e != "" { + a = e + } + if e := equivEscapers[b]; e != "" { + b = e + } + return a == b +} + // redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x) // for all x. var redundantFuncs = map[string]map[string]bool{ @@ -318,17 +371,6 @@ func indexOfStr(s string, strs []string, eq func(a, b string) bool) int { return -1 } -// escFnsEq reports whether the two escaping functions are equivalent. -func escFnsEq(a, b string) bool { - if e := equivEscapers[a]; e != "" { - a = e - } - if e := equivEscapers[b]; e != "" { - b = e - } - return a == b -} - // newIdentCmd produces a command containing a single identifier node. func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode { return &parse.CommandNode{ @@ -449,13 +491,13 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context { // It returns the best guess at an output context, and the result of the filter // which is the same as whether e was updated. func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) { - e1 := newEscaper(e.tmpl) + e1 := makeEscaper(e.ns) // Make type inferences available to f. for k, v := range e.output { e1.output[k] = v } c = e1.escapeList(c, n) - ok := filter != nil && filter(e1, c) + ok := filter != nil && filter(&e1, c) if ok { // Copy inferences and edits from e1 back into e. for k, v := range e1.output { @@ -504,7 +546,7 @@ func (e *escaper) escapeTree(c context, node parse.Node, name string, line int) if t == nil { // Two cases: The template exists but is empty, or has never been mentioned at // all. Distinguish the cases in the error messages. - if e.tmpl.set[name] != nil { + if e.ns.set[name] != nil { return context{ state: stateError, err: errorf(ErrNoSuchTemplate, node, line, "%q is an incomplete or empty template", name), @@ -624,7 +666,7 @@ func (e *escaper) escapeText(c context, n *parse.TextNode) context { // the entire comment is considered to be a // LineTerminator for purposes of parsing by // the syntactic grammar." - if bytes.IndexAny(s[written:i1], "\n\r\u2028\u2029") != -1 { + if bytes.ContainsAny(s[written:i1], "\n\r\u2028\u2029") { b.WriteByte('\n') } else { b.WriteByte(' ') @@ -752,8 +794,11 @@ func (e *escaper) commit() { for name := range e.output { e.template(name).Funcs(funcMap) } + // Any template from the name space associated with this escaper can be used + // to add derived templates to the underlying text/template name space. + tmpl := e.arbitraryTemplate() for _, t := range e.derived { - if _, err := e.tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil { + if _, err := tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil { panic("error adding derived template") } } @@ -766,17 +811,34 @@ func (e *escaper) commit() { for n, s := range e.textNodeEdits { n.Text = s } + // Reset state that is specific to this commit so that the same changes are + // not re-applied to the template on subsequent calls to commit. + e.called = make(map[string]bool) + e.actionNodeEdits = make(map[*parse.ActionNode][]string) + e.templateNodeEdits = make(map[*parse.TemplateNode]string) + e.textNodeEdits = make(map[*parse.TextNode][]byte) } // template returns the named template given a mangled template name. func (e *escaper) template(name string) *template.Template { - t := e.tmpl.text.Lookup(name) + // Any template from the name space associated with this escaper can be used + // to look up templates in the underlying text/template name space. + t := e.arbitraryTemplate().text.Lookup(name) if t == nil { t = e.derived[name] } return t } +// arbitraryTemplate returns an arbitrary template from the name space +// associated with e and panics if no templates are found. +func (e *escaper) arbitraryTemplate() *Template { + for _, t := range e.ns.set { + return t + } + panic("no templates in name space") +} + // Forwarding functions so that clients need only import this package // to reach the general escaping functions of text/template. diff --git a/libgo/go/html/template/escape_test.go b/libgo/go/html/template/escape_test.go index f6ace496e7b..f5a4ce17364 100644 --- a/libgo/go/html/template/escape_test.go +++ b/libgo/go/html/template/escape_test.go @@ -359,7 +359,7 @@ func TestEscape(t *testing.T) { { "styleStrEncodedProtocolEncoded", `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`, - // The CSS string 'javascript\\3a alert(1337)' does not contains a colon. + // The CSS string 'javascript\\3a alert(1337)' does not contain a colon. `<a style="background: 'javascript\\3a alert\28 1337\29 '">`, }, { @@ -685,6 +685,40 @@ func TestEscape(t *testing.T) { } } +func TestEscapeMap(t *testing.T) { + data := map[string]string{ + "html": `<h1>Hi!</h1>`, + "urlquery": `http://www.foo.com/index.html?title=main`, + } + for _, test := range [...]struct { + desc, input, output string + }{ + // covering issue 20323 + { + "field with predefined escaper name 1", + `{{.html | print}}`, + `<h1>Hi!</h1>`, + }, + // covering issue 20323 + { + "field with predefined escaper name 2", + `{{.urlquery | print}}`, + `http://www.foo.com/index.html?title=main`, + }, + } { + tmpl := Must(New("").Parse(test.input)) + b := new(bytes.Buffer) + if err := tmpl.Execute(b, data); err != nil { + t.Errorf("%s: template execution failed: %s", test.desc, err) + continue + } + if w, g := test.output, b.String(); w != g { + t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g) + continue + } + } +} + func TestEscapeSet(t *testing.T) { type dataItem struct { Children []*dataItem @@ -970,8 +1004,33 @@ func TestErrors(t *testing.T) { `<a=foo>`, `: expected space, attr name, or end of tag, but got "=foo>"`, }, + { + `Hello, {{. | urlquery | print}}!`, + // urlquery is disallowed if it is not the last command in the pipeline. + `predefined escaper "urlquery" disallowed in template`, + }, + { + `Hello, {{. | html | print}}!`, + // html is disallowed if it is not the last command in the pipeline. + `predefined escaper "html" disallowed in template`, + }, + { + `Hello, {{html . | print}}!`, + // A direct call to html is disallowed if it is not the last command in the pipeline. + `predefined escaper "html" disallowed in template`, + }, + { + `<div class={{. | html}}>Hello<div>`, + // html is disallowed in a pipeline that is in an unquoted attribute context, + // even if it is the last command in the pipeline. + `predefined escaper "html" disallowed in template`, + }, + { + `Hello, {{. | urlquery | html}}!`, + // html is allowed since it is the last command in the pipeline, but urlquery is not. + `predefined escaper "urlquery" disallowed in template`, + }, } - for _, test := range tests { buf := new(bytes.Buffer) tmpl, err := New("z").Parse(test.input) @@ -1396,6 +1455,16 @@ func TestEscapeText(t *testing.T) { `<script type="text/template">`, context{state: stateText}, }, + // covering issue 19968 + { + `<script type="TEXT/JAVASCRIPT">`, + context{state: stateJS, element: elementScript}, + }, + // covering issue 19965 + { + `<script TYPE="text/template">`, + context{state: stateText}, + }, { `<script type="notjs">`, context{state: stateText}, @@ -1495,7 +1564,7 @@ func TestEscapeText(t *testing.T) { } for _, test := range tests { - b, e := []byte(test.input), newEscaper(nil) + b, e := []byte(test.input), makeEscaper(nil) c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) if !test.output.eq(c) { t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) @@ -1529,24 +1598,24 @@ func TestEnsurePipelineContains(t *testing.T) { []string{"html"}, }, { - "{{.X | html}}", - ".X | html | urlquery", - []string{"urlquery"}, + "{{html .X}}", + "_eval_args_ .X | html | urlquery", + []string{"html", "urlquery"}, }, { - "{{.X | html | urlquery}}", - ".X | html | urlquery", - []string{"urlquery"}, + "{{html .X .Y .Z}}", + "_eval_args_ .X .Y .Z | html | urlquery", + []string{"html", "urlquery"}, }, { - "{{.X | html | urlquery}}", - ".X | html | urlquery", - []string{"html", "urlquery"}, + "{{.X | print}}", + ".X | print | urlquery", + []string{"urlquery"}, }, { - "{{.X | html | urlquery}}", - ".X | html | urlquery", - []string{"html"}, + "{{.X | print | urlquery}}", + ".X | print | urlquery", + []string{"urlquery"}, }, { "{{.X | urlquery}}", @@ -1554,50 +1623,69 @@ func TestEnsurePipelineContains(t *testing.T) { []string{"html", "urlquery"}, }, { - "{{.X | html | print}}", - ".X | urlquery | html | print", + "{{.X | print 2 | .f 3}}", + ".X | print 2 | .f 3 | urlquery | html", []string{"urlquery", "html"}, }, { - "{{($).X | html | print}}", - "($).X | urlquery | html | print", + // covering issue 10801 + "{{.X | println.x }}", + ".X | println.x | urlquery | html", []string{"urlquery", "html"}, }, { - "{{.X | print 2 | .f 3}}", - ".X | print 2 | .f 3 | urlquery | html", + // covering issue 10801 + "{{.X | (print 12 | println).x }}", + ".X | (print 12 | println).x | urlquery | html", []string{"urlquery", "html"}, }, + // The following test cases ensure that the merging of internal escapers + // with the predefined "html" and "urlquery" escapers is correct. { - "{{.X | html | print 2 | .f 3}}", - ".X | urlquery | html | print 2 | .f 3", - []string{"urlquery", "html"}, + "{{.X | urlquery}}", + ".X | _html_template_urlfilter | urlquery", + []string{"_html_template_urlfilter", "_html_template_urlnormalizer"}, }, { - // covering issue 10801 - "{{.X | js.x }}", - ".X | js.x | urlquery | html", - []string{"urlquery", "html"}, + "{{.X | urlquery}}", + ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper", + []string{"_html_template_urlfilter", "_html_template_cssescaper"}, }, { - // covering issue 10801 - "{{.X | (print 12 | js).x }}", - ".X | (print 12 | js).x | urlquery | html", - []string{"urlquery", "html"}, + "{{.X | urlquery}}", + ".X | urlquery", + []string{"_html_template_urlnormalizer"}, + }, + { + "{{.X | urlquery}}", + ".X | urlquery", + []string{"_html_template_urlescaper"}, + }, + { + "{{.X | html}}", + ".X | html", + []string{"_html_template_htmlescaper"}, + }, + { + "{{.X | html}}", + ".X | html", + []string{"_html_template_rcdataescaper"}, }, } for i, test := range tests { tmpl := template.Must(template.New("test").Parse(test.input)) action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) if !ok { - t.Errorf("#%d: First node is not an action: %s", i, test.input) + t.Errorf("First node is not an action: %s", test.input) continue } pipe := action.Pipe + originalIDs := make([]string, len(test.ids)) + copy(originalIDs, test.ids) ensurePipelineContains(pipe, test.ids) got := pipe.String() if got != test.output { - t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got) + t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got) } } } @@ -1606,10 +1694,8 @@ func TestEscapeMalformedPipelines(t *testing.T) { tests := []string{ "{{ 0 | $ }}", "{{ 0 | $ | urlquery }}", - "{{ 0 | $ | urlquery | html }}", "{{ 0 | (nil) }}", "{{ 0 | (nil) | html }}", - "{{ 0 | (nil) | html | urlquery }}", } for _, test := range tests { var b bytes.Buffer @@ -1761,6 +1847,40 @@ func TestErrorOnUndefined(t *testing.T) { } } +// This covers issue #20842. +func TestIdempotentExecute(t *testing.T) { + tmpl := Must(New(""). + Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) + Must(tmpl. + Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) + got := new(bytes.Buffer) + var err error + // Ensure that "hello" produces the same output when executed twice. + want := "Hello, Ladies & Gentlemen!" + for i := 0; i < 2; i++ { + err = tmpl.ExecuteTemplate(got, "hello", nil) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if got.String() != want { + t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) + } + got.Reset() + } + // Ensure that the implicit re-execution of "hello" during the execution of + // "main" does not cause the output of "hello" to change. + err = tmpl.ExecuteTemplate(got, "main", nil) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + // If the HTML escaper is added again to the action {{"Ladies & Gentlemen!"}}, + // we would expected to see the ampersand overescaped to "&amp;". + want = "<body>Hello, Ladies & Gentlemen!</body>" + if got.String() != want { + t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) + } +} + func BenchmarkEscapedExecute(b *testing.B) { tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) var buf bytes.Buffer diff --git a/libgo/go/html/template/js.go b/libgo/go/html/template/js.go index 6434fa3be63..239395f8d3a 100644 --- a/libgo/go/html/template/js.go +++ b/libgo/go/html/template/js.go @@ -372,7 +372,7 @@ func isJSType(mimeType string) bool { // https://tools.ietf.org/html/rfc7231#section-3.1.1 // https://tools.ietf.org/html/rfc4329#section-3 // https://www.ietf.org/rfc/rfc4627.txt - + mimeType = strings.ToLower(mimeType) // discard parameters if i := strings.Index(mimeType, ";"); i >= 0 { mimeType = mimeType[:i] diff --git a/libgo/go/html/template/template.go b/libgo/go/html/template/template.go index b313a6b104a..6a661bf6e5d 100644 --- a/libgo/go/html/template/template.go +++ b/libgo/go/html/template/template.go @@ -36,6 +36,7 @@ type nameSpace struct { mu sync.Mutex set map[string]*Template escaped bool + esc escaper } // Templates returns a slice of the templates associated with t, including t @@ -112,7 +113,8 @@ func (t *Template) escape() error { // If an error occurs executing the template or writing its output, // execution stops, but partial results may already have been written to // the output writer. -// A template may be executed safely in parallel. +// A template may be executed safely in parallel, although if parallel +// executions share a Writer the output may be interleaved. func (t *Template) Execute(wr io.Writer, data interface{}) error { if err := t.escape(); err != nil { return err @@ -125,7 +127,8 @@ func (t *Template) Execute(wr io.Writer, data interface{}) error { // If an error occurs executing the template or writing its output, // execution stops, but partial results may already have been written to // the output writer. -// A template may be executed safely in parallel. +// A template may be executed safely in parallel, although if parallel +// executions share a Writer the output may be interleaved. func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { tmpl, err := t.lookupAndEscapeTemplate(name) if err != nil { @@ -248,13 +251,13 @@ func (t *Template) Clone() (*Template, error) { if err != nil { return nil, err } + ns := &nameSpace{set: make(map[string]*Template)} + ns.esc = makeEscaper(ns) ret := &Template{ nil, textClone, textClone.Tree, - &nameSpace{ - set: make(map[string]*Template), - }, + ns, } ret.set[ret.Name()] = ret for _, x := range textClone.Templates() { @@ -277,13 +280,13 @@ func (t *Template) Clone() (*Template, error) { // New allocates a new HTML template with the given name. func New(name string) *Template { + ns := &nameSpace{set: make(map[string]*Template)} + ns.esc = makeEscaper(ns) tmpl := &Template{ nil, template.New(name), nil, - &nameSpace{ - set: make(map[string]*Template), - }, + ns, } tmpl.set[name] = tmpl return tmpl @@ -325,6 +328,7 @@ func (t *Template) Name() string { type FuncMap map[string]interface{} // Funcs adds the elements of the argument map to the template's function map. +// It must be called before the template is parsed. // It panics if a value in the map is not a function with appropriate return // type. However, it is legal to overwrite elements of the map. The return // value is the template, so calls can be chained. diff --git a/libgo/go/html/template/transition.go b/libgo/go/html/template/transition.go index 4a4716d7820..df7ac2289b4 100644 --- a/libgo/go/html/template/transition.go +++ b/libgo/go/html/template/transition.go @@ -106,7 +106,7 @@ func tTag(c context, s []byte) (context, int) { }, len(s) } - attrName := string(s[i:j]) + attrName := strings.ToLower(string(s[i:j])) if c.element == elementScript && attrName == "type" { attr = attrScriptType } else { @@ -246,7 +246,7 @@ func tAttr(c context, s []byte) (context, int) { // tURL is the context transition function for the URL state. func tURL(c context, s []byte) (context, int) { - if bytes.IndexAny(s, "#?") >= 0 { + if bytes.ContainsAny(s, "#?") { c.urlPart = urlPartQueryOrFrag } else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone { // HTML5 uses "Valid URL potentially surrounded by spaces" for |