Commit 4c6454ae authored by Mike Samuel's avatar Mike Samuel

exp/template/html: escape {{template}} calls and sets of templates

This adds support for {{template "callee"}} calls.
It recognizes that calls can appear in many contexts.

{{if .ImageURL}}
    <img src="{{.ImageURL}}" alt="{{template "description"}}">
{{else}}
    <p>{{template "description"}}</p>
{{end}}

calls a template in two different contexts, first in an HTML attribute
context, and second in an HTML text context.

Those two contexts aren't very different, but when linking text
to search terms, the escaping context can be materially different:

<a href="/search?q={{template "tags"}}">{{template "tags"}}</a>

This adds API:
EscapeSet(*template.Set, names ...string) os.Error

takes a set of templates and the names of those which might be called
in the default context as starting points.

It changes the escape* functions to be methods of an object which
maintains a conceptual mapping of
(template names*input context) -> output context.

The actual mapping uses as key a mangled name which combines the
template name with the input context.

The mangled name when the input context is the default context is the
same as the unmangled name.

When a template is called in multiple contexts, we clone the template.

{{define "tagLink"}}
  <a href="/search?q={{template "tags"}}">{{template "tags"}}</a>
{{end}}
{{define "tags"}}
  {{range .Tags}}{{.}},{{end}}
{{end}}

given []string{ "foo", "O'Reilly", "bar" } produces

  <a href="/search?q=foo,O%27Reilly,bar">foo,O&#39;Reilly,bar</a>

This involves rewriting the above to something like

{{define "tagLink"}}
  <a href="/search?q={{template "tags$1"}}">{{template "tags"}}</a>
{{end}}
{{define "tags"}}
  {{range .Tags}}{{. | html}},{{end}}
{{end}}
{{define "tags$1"}}
  {{range .Tags}}{{. | urlquery}},{{end}}
{{end}}

clone.go provides a mechanism for cloning template "tags" to produce
"tags$1".

changes to escape.go implement the new API and context propagation
around the call graph.

context.go includes minor changes to support name mangling and
context_test.go tests those.

js.go contains a bug-fix.

R=nigeltao, r
CC=golang-dev
https://golang.org/cl/4969072
parent 9377b288
......@@ -6,6 +6,7 @@ include ../../../../Make.inc
TARG=exp/template/html
GOFILES=\
clone.go\
context.go\
css.go\
escape.go\
......
// 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 (
"template/parse"
)
// clone clones a template Node.
func clone(n parse.Node) parse.Node {
switch t := n.(type) {
case *parse.ActionNode:
return cloneAction(t)
case *parse.IfNode:
b := new(parse.IfNode)
copyBranch(&b.BranchNode, &t.BranchNode)
return b
case *parse.ListNode:
return cloneList(t)
case *parse.RangeNode:
b := new(parse.RangeNode)
copyBranch(&b.BranchNode, &t.BranchNode)
return b
case *parse.TemplateNode:
return cloneTemplate(t)
case *parse.TextNode:
return cloneText(t)
case *parse.WithNode:
b := new(parse.WithNode)
copyBranch(&b.BranchNode, &t.BranchNode)
return b
}
panic("cloning " + n.String() + " is unimplemented")
}
// cloneAction returns a deep clone of n.
func cloneAction(n *parse.ActionNode) *parse.ActionNode {
// We use keyless fields because they won't compile if a field is added.
return &parse.ActionNode{n.NodeType, n.Line, clonePipe(n.Pipe)}
}
// cloneList returns a deep clone of n.
func cloneList(n *parse.ListNode) *parse.ListNode {
if n == nil {
return nil
}
// We use keyless fields because they won't compile if a field is added.
c := parse.ListNode{n.NodeType, make([]parse.Node, len(n.Nodes))}
for i, child := range n.Nodes {
c.Nodes[i] = clone(child)
}
return &c
}
// clonePipe returns a shallow clone of n.
// The escaper does not modify pipe descendants in place so there's no need to
// clone deeply.
func clonePipe(n *parse.PipeNode) *parse.PipeNode {
if n == nil {
return nil
}
// We use keyless fields because they won't compile if a field is added.
return &parse.PipeNode{n.NodeType, n.Line, n.Decl, n.Cmds}
}
// cloneTemplate returns a deep clone of n.
func cloneTemplate(n *parse.TemplateNode) *parse.TemplateNode {
// We use keyless fields because they won't compile if a field is added.
return &parse.TemplateNode{n.NodeType, n.Line, n.Name, clonePipe(n.Pipe)}
}
// cloneText clones the given node sharing its []byte.
func cloneText(n *parse.TextNode) *parse.TextNode {
// We use keyless fields because they won't compile if a field is added.
return &parse.TextNode{n.NodeType, n.Text}
}
// copyBranch clones src into dst.
func copyBranch(dst, src *parse.BranchNode) {
// We use keyless fields because they won't compile if a field is added.
*dst = parse.BranchNode{
src.NodeType,
src.Line,
clonePipe(src.Pipe),
cloneList(src.List),
cloneList(src.ElseList),
}
}
// 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"
"template/parse"
"testing"
)
func TestClone(t *testing.T) {
tests := []struct {
input, want, wantClone string
}{
{
`Hello, {{if true}}{{"<World>"}}{{end}}!`,
"Hello, <World>!",
"Hello, &lt;World&gt;!",
},
{
`Hello, {{if false}}{{.X}}{{else}}{{"<World>"}}{{end}}!`,
"Hello, <World>!",
"Hello, &lt;World&gt;!",
},
{
`Hello, {{with "<World>"}}{{.}}{{end}}!`,
"Hello, <World>!",
"Hello, &lt;World&gt;!",
},
{
`{{range .}}<p>{{.}}</p>{{end}}`,
"<p>foo</p><p><bar></p><p>baz</p>",
"<p>foo</p><p>&lt;bar&gt;</p><p>baz</p>",
},
{
`Hello, {{"<World>" | html}}!`,
"Hello, &lt;World&gt;!",
"Hello, &lt;World&gt;!",
},
{
`Hello{{if 1}}, World{{else}}{{template "d"}}{{end}}!`,
"Hello, World!",
"Hello, World!",
},
}
for _, test := range tests {
s := template.Must(template.New("s").Parse(test.input))
d := template.New("d")
d.Tree = &parse.Tree{Name: d.Name(), Root: cloneList(s.Root)}
if want, got := s.Root.String(), d.Root.String(); want != got {
t.Errorf("want %q, got %q", want, got)
}
d, err := Escape(d)
if err != nil {
t.Errorf("%q: failed to escape: %s", test.input, err)
continue
}
if want, got := "s", s.Name(); want != got {
t.Errorf("want %q, got %q", want, got)
continue
}
if want, got := "d", d.Name(); want != got {
t.Errorf("want %q, got %q", want, got)
continue
}
data := []string{"foo", "<bar>", "baz"}
// Make sure escaping d did not affect s.
var b bytes.Buffer
s.Execute(&b, data)
if got := b.String(); got != test.want {
t.Errorf("%q: want %q, got %q", test.input, test.want, got)
continue
}
b.Reset()
d.Execute(&b, data)
if got := b.String(); got != test.wantClone {
t.Errorf("%q: want %q, got %q", test.input, test.wantClone, got)
}
}
}
......@@ -36,6 +36,29 @@ func (c context) eq(d context) bool {
c.errStr == d.errStr
}
// mangle produces an identifier that includes a suffix that distinguishes it
// from template names mangled with different contexts.
func (c context) mangle(templateName string) string {
// The mangled name for the default context is the input templateName.
if c.state == stateText {
return templateName
}
s := templateName + "$htmltemplate_" + c.state.String()
if c.delim != 0 {
s += "_" + c.delim.String()
}
if c.urlPart != 0 {
s += "_" + c.urlPart.String()
}
if c.jsCtx != 0 {
s += "_" + c.jsCtx.String()
}
if c.element != 0 {
s += "_" + c.element.String()
}
return s
}
// state describes a high-level HTML parser state.
//
// It bounds the top of the element stack, and by extension the HTML insertion
......
This diff is collapsed.
......@@ -6,6 +6,7 @@ package html
import (
"bytes"
"os"
"strings"
"template"
"template/parse"
......@@ -374,6 +375,128 @@ func TestEscape(t *testing.T) {
}
}
func TestEscapeSet(t *testing.T) {
type dataItem struct {
Children []*dataItem
X string
}
data := dataItem{
Children: []*dataItem{
&dataItem{X: "foo"},
&dataItem{X: "<bar>"},
&dataItem{
Children: []*dataItem{
&dataItem{X: "baz"},
},
},
},
}
tests := []struct {
inputs map[string]string
want string
}{
// The trivial set.
{
map[string]string{
"main": ``,
},
``,
},
// A template called in the start context.
{
map[string]string{
"main": `Hello, {{template "helper"}}!`,
// Not a valid top level HTML template.
// "<b" is not a full tag.
"helper": `{{"<World>"}}`,
},
`Hello, &lt;World&gt;!`,
},
// A template called in a context other than the start.
{
map[string]string{
"main": `<a onclick='a = {{template "helper"}};'>`,
// Not a valid top level HTML template.
// "<b" is not a full tag.
"helper": `{{"<a>"}}<b`,
},
`<a onclick='a = &#34;\u003ca\u003e&#34;<b;'>`,
},
// A recursive template that ends in its start context.
{
map[string]string{
"main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`,
},
`foo &lt;bar&gt; baz `,
},
// A recursive helper template that ends in its start context.
{
map[string]string{
"main": `{{template "helper" .}}`,
"helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`,
},
`<ul><li>foo</li><li>&lt;bar&gt;</li><li><ul><li>baz</li></ul></li></ul>`,
},
// Co-recursive templates that end in its start context.
{
map[string]string{
"main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`,
"helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`,
},
`<blockquote>foo<br>&lt;bar&gt;<br><blockquote>baz<br></blockquote></blockquote>`,
},
// A template that is called in two different contexts.
{
map[string]string{
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
"helper": `{{11}} of {{"<100>"}}`,
},
`<button onclick="title='11 of \x3c100\x3e'; ...">11 of &lt;100&gt;</button>`,
},
// A non-recursive template that ends in a different context.
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.
{
map[string]string{
"main": `<script>var x={{template "helper"}}/{{"42"}};</script>`,
"helper": "{{126}}",
},
`<script>var x= 126 /"42";</script>`,
},
// A recursive template that ends in a different context.
/*
{
map[string]string{
"main": `<a href="/foo{{template "helper" .}}">`,
"helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`,
},
`<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`,
},
*/
}
for _, test := range tests {
var s template.Set
for name, src := range test.inputs {
s.Add(template.Must(template.New(name).Parse(src)))
}
if _, err := EscapeSet(&s, "main"); err != nil {
t.Errorf("%s for input:\n%v", err, test.inputs)
continue
}
var b bytes.Buffer
if err := s.Execute(&b, "main", data); err != nil {
t.Errorf("%q executing %v", err.String(), s.Template("main"))
continue
}
if got := b.String(); test.want != got {
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
}
}
}
func TestErrors(t *testing.T) {
tests := []struct {
input string
......@@ -496,12 +619,40 @@ func TestErrors(t *testing.T) {
`<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,
`: '/' could start div or regexp: "/-"`,
},
{
`{{template "foo"}}`,
"z:1: no such template foo",
},
{
`{{define "z"}}<div{{template "y"}}>{{end}}` +
// Illegal starting in stateTag but not in stateText.
`{{define "y"}} foo<b{{end}}`,
`z:0: "<" in attribute name: " foo<b"`,
},
{
`{{define "z"}}<script>reverseList = [{{template "t"}}]</script>{{end}}` +
// Missing " after recursive call.
`{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`,
`: cannot compute output context for template t$htmltemplate_stateJS_elementScript`,
},
}
for _, test := range tests {
tmpl := template.Must(template.New("z").Parse(test.input))
var err os.Error
if strings.HasPrefix(test.input, "{{define") {
var s template.Set
_, err = s.Parse(test.input)
if err != nil {
t.Errorf("Failed to parse %q: %s", test.input, err)
continue
}
_, err = EscapeSet(&s, "z")
} else {
tmpl := template.Must(template.New("z").Parse(test.input))
_, err = Escape(tmpl)
}
var got string
if _, err := Escape(tmpl); err != nil {
if err != nil {
got = err.String()
}
if test.err == "" {
......@@ -715,6 +866,10 @@ func TestEscapeText(t *testing.T) {
`<a onclick="/foo/`,
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
},
{
`<script>/foo/ /=`,
context{state: stateJS, element: elementScript},
},
{
`<a onclick="1 /foo`,
context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
......@@ -914,8 +1069,8 @@ func TestEscapeText(t *testing.T) {
}
for _, test := range tests {
b := []byte(test.input)
c := escapeText(context{}, b)
b, e := []byte(test.input), escaper{}
c := e.escapeText(context{}, b)
if !test.output.eq(c) {
t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c)
continue
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment