diff options
Diffstat (limited to 'src/pkg/exp/template')
-rw-r--r-- | src/pkg/exp/template/html/Makefile | 11 | ||||
-rw-r--r-- | src/pkg/exp/template/html/context.go | 98 | ||||
-rw-r--r-- | src/pkg/exp/template/html/escape.go | 105 | ||||
-rw-r--r-- | src/pkg/exp/template/html/escape_test.go | 75 |
4 files changed, 289 insertions, 0 deletions
diff --git a/src/pkg/exp/template/html/Makefile b/src/pkg/exp/template/html/Makefile new file mode 100644 index 000000000..2f107da11 --- /dev/null +++ b/src/pkg/exp/template/html/Makefile @@ -0,0 +1,11 @@ +# Copyright 2011 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include ../../../../Make.inc + +TARG=exp/template/html +GOFILES=\ + escape.go + +include ../../../../Make.pkg diff --git a/src/pkg/exp/template/html/context.go b/src/pkg/exp/template/html/context.go new file mode 100644 index 000000000..411006883 --- /dev/null +++ b/src/pkg/exp/template/html/context.go @@ -0,0 +1,98 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "fmt" +) + +// context describes the state an HTML parser must be in when it reaches the +// portion of HTML produced by evaluating a particular template node. +// +// The zero value of type context is the start context for a template that +// produces an HTML fragment as defined at +// http://www.w3.org/TR/html5/the-end.html#parsing-html-fragments +// where the context element is null. +type context struct { + state state + delim delim +} + +func (c context) String() string { + return fmt.Sprintf("context{state: %s, delim: %s", c.state, c.delim) +} + +// eq is true if the two contexts are identical field-wise. +func (c context) eq(d context) bool { + return c.state == d.state && c.delim == d.delim +} + +// state describes a high-level HTML parser state. +// +// It bounds the top of the element stack, and by extension the HTML +// insertion mode, but also contains state that does not correspond to +// anything in the HTML5 parsing algorithm because a single token +// production in the HTML grammar may contain embedded actions in a template. +// For instance, the quoted HTML attribute produced by +// <div title="Hello {{.World}}"> +// is a single token in HTML's grammar but in a template spans several nodes. +type state uint8 + +const ( + // statePCDATA is parsed character data. An HTML parser is in + // this state when its parse position is outside an HTML tag, + // directive, comment, and special element body. + statePCDATA state = iota + // stateTag occurs before an HTML attribute or the end of a tag. + stateTag + // stateURI occurs inside an HTML attribute whose content is a URI. + stateURI + // stateError is an infectious error state outside any valid + // HTML/CSS/JS construct. + stateError +) + +var stateNames = [...]string{ + statePCDATA: "statePCDATA", + stateTag: "stateTag", + stateURI: "stateURI", + stateError: "stateError", +} + +func (s state) String() string { + if uint(s) < uint(len(stateNames)) { + return stateNames[s] + } + return fmt.Sprintf("illegal state %d", uint(s)) +} + +// delim is the delimiter that will end the current HTML attribute. +type delim uint8 + +const ( + // delimNone occurs outside any attribute. + delimNone delim = iota + // delimDoubleQuote occurs when a double quote (") closes the attribute. + delimDoubleQuote + // delimSingleQuote occurs when a single quote (') closes the attribute. + delimSingleQuote + // delimSpaceOrTagEnd occurs when a space or right angle bracket (>) + // closes the attribute. + delimSpaceOrTagEnd +) + +var delimNames = [...]string{ + delimNone: "delimNone", + delimDoubleQuote: "delimDoubleQuote", + delimSingleQuote: "delimSingleQuote", + delimSpaceOrTagEnd: "delimSpaceOrTagEnd", +} + +func (d delim) String() string { + if uint(d) < uint(len(delimNames)) { + return delimNames[d] + } + return fmt.Sprintf("illegal delim %d", uint(d)) +} diff --git a/src/pkg/exp/template/html/escape.go b/src/pkg/exp/template/html/escape.go new file mode 100644 index 000000000..e0e87b98d --- /dev/null +++ b/src/pkg/exp/template/html/escape.go @@ -0,0 +1,105 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package html is a specialization of exp/template that automates the +// construction of safe HTML output. +// At the moment, the escaping is naive. All dynamic content is assumed to be +// plain text interpolated in an HTML PCDATA context. +package html + +import ( + "template" + "template/parse" +) + +// Escape rewrites each action in the template to guarantee the output is +// HTML-escaped. +func Escape(t *template.Template) { + // If the parser shares trees based on common-subexpression + // joining then we will need to avoid multiply escaping the same action. + escapeListNode(t.Tree.Root) +} + +// escapeNode dispatches to escape<NodeType> helpers by type. +func escapeNode(node parse.Node) { + switch n := node.(type) { + case *parse.ListNode: + escapeListNode(n) + case *parse.TextNode: + // Nothing to do. + case *parse.ActionNode: + escapeActionNode(n) + case *parse.IfNode: + escapeIfNode(n) + case *parse.RangeNode: + escapeRangeNode(n) + case *parse.TemplateNode: + // Nothing to do. + case *parse.WithNode: + escapeWithNode(n) + default: + panic("handling for " + node.String() + " not implemented") + // TODO: Handle other inner node types. + } +} + +// escapeListNode recursively escapes its input's children. +func escapeListNode(node *parse.ListNode) { + if node == nil { + return + } + children := node.Nodes + for _, child := range children { + escapeNode(child) + } +} + +// escapeActionNode adds a pipeline call to the end that escapes the result +// of the expression before it is interpolated into the template output. +func escapeActionNode(node *parse.ActionNode) { + pipe := node.Pipe + + cmds := pipe.Cmds + nCmds := len(cmds) + + // If it already has an escaping command, do not interfere. + if nCmds != 0 { + if lastCmd := cmds[nCmds-1]; len(lastCmd.Args) != 0 { + // TODO: Recognize url and js as escaping functions once + // we have enough context to know whether additional + // escaping is necessary. + if arg, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok && arg.Ident == "html" { + return + } + } + } + + htmlEscapeCommand := parse.CommandNode{ + NodeType: parse.NodeCommand, + Args: []parse.Node{parse.NewIdentifier("html")}, + } + + node.Pipe.Cmds = append(node.Pipe.Cmds, &htmlEscapeCommand) +} + +// escapeIfNode recursively escapes the if and then clauses but leaves the +// condition unchanged. +func escapeIfNode(node *parse.IfNode) { + escapeListNode(node.List) + escapeListNode(node.ElseList) +} + +// escapeRangeNode recursively escapes the loop body and else clause but +// leaves the series unchanged. +func escapeRangeNode(node *parse.RangeNode) { + escapeListNode(node.List) + escapeListNode(node.ElseList) +} + +// escapeWithNode recursively escapes the scope body and else clause but +// leaves the pipeline unchanged. +func escapeWithNode(node *parse.WithNode) { + escapeListNode(node.List) + escapeListNode(node.ElseList) +} diff --git a/src/pkg/exp/template/html/escape_test.go b/src/pkg/exp/template/html/escape_test.go new file mode 100644 index 000000000..345a752a8 --- /dev/null +++ b/src/pkg/exp/template/html/escape_test.go @@ -0,0 +1,75 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "bytes" + "template" + "testing" +) + +type data struct { + F, T bool + C, G, H string + A, E []string +} + +var testData = data{ + F: false, + T: true, + C: "<Cincinatti>", + G: "<Goodbye>", + H: "<Hello>", + A: []string{"<a>", "<b>"}, + E: []string{}, +} + +type testCase struct { + name string + input string + output string +} + +var testCases = []testCase{ + {"if", "{{if .T}}Hello{{end}}, {{.C}}!", "Hello, <Cincinatti>!"}, + {"else", "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", "<Goodbye>!"}, + {"overescaping", "Hello, {{.C | html}}!", "Hello, <Cincinatti>!"}, + {"assignment", "{{if $x := .H}}{{$x}}{{end}}", "<Hello>"}, + {"withBody", "{{with .H}}{{.}}{{end}}", "<Hello>"}, + {"withElse", "{{with .E}}{{.}}{{else}}{{.H}}{{end}}", "<Hello>"}, + {"rangeBody", "{{range .A}}{{.}}{{end}}", "<a><b>"}, + {"rangeElse", "{{range .E}}{{.}}{{else}}{{.H}}{{end}}", "<Hello>"}, + {"nonStringValue", "{{.T}}", "true"}, + {"constant", `<a href="{{"'str'"}}">`, `<a href="'str'">`}, +} + +func TestAutoesc(t *testing.T) { + for _, testCase := range testCases { + name := testCase.name + tmpl := template.New(name) + tmpl, err := tmpl.Parse(testCase.input) + if err != nil { + t.Errorf("%s: failed to parse template: %s", name, err) + continue + } + + Escape(tmpl) + + buffer := new(bytes.Buffer) + + err = tmpl.Execute(buffer, testData) + if err != nil { + t.Errorf("%s: template execution failed: %s", name, err) + continue + } + + output := testCase.output + actual := buffer.String() + if output != actual { + t.Errorf("%s: escaped output: %q != %q", + name, output, actual) + } + } +} |