diff options
Diffstat (limited to 'src/pkg/html')
-rw-r--r-- | src/pkg/html/escape.go | 10 | ||||
-rw-r--r-- | src/pkg/html/escape_test.go | 97 | ||||
-rw-r--r-- | src/pkg/html/template/clone_test.go | 40 | ||||
-rw-r--r-- | src/pkg/html/template/content.go | 6 | ||||
-rw-r--r-- | src/pkg/html/template/content_test.go | 31 | ||||
-rw-r--r-- | src/pkg/html/template/context.go | 2 | ||||
-rw-r--r-- | src/pkg/html/template/css.go | 8 | ||||
-rw-r--r-- | src/pkg/html/template/escape.go | 4 | ||||
-rw-r--r-- | src/pkg/html/template/escape_test.go | 11 | ||||
-rw-r--r-- | src/pkg/html/template/js.go | 2 | ||||
-rw-r--r-- | src/pkg/html/template/template.go | 18 | ||||
-rw-r--r-- | src/pkg/html/template/transition.go | 4 |
12 files changed, 200 insertions, 33 deletions
diff --git a/src/pkg/html/escape.go b/src/pkg/html/escape.go index eff0384e0..dd5dfa7cd 100644 --- a/src/pkg/html/escape.go +++ b/src/pkg/html/escape.go @@ -187,16 +187,6 @@ func unescape(b []byte) []byte { return b } -// lower lower-cases the A-Z bytes in b in-place, so that "aBc" becomes "abc". -func lower(b []byte) []byte { - for i, c := range b { - if 'A' <= c && c <= 'Z' { - b[i] = c + 'a' - 'A' - } - } - return b -} - const escapedChars = `&'<>"` func escape(w writer, s string) error { diff --git a/src/pkg/html/escape_test.go b/src/pkg/html/escape_test.go new file mode 100644 index 000000000..b405d4b4a --- /dev/null +++ b/src/pkg/html/escape_test.go @@ -0,0 +1,97 @@ +// Copyright 2013 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 "testing" + +type unescapeTest struct { + // A short description of the test case. + desc string + // The HTML text. + html string + // The unescaped text. + unescaped string +} + +var unescapeTests = []unescapeTest{ + // Handle no entities. + { + "copy", + "A\ttext\nstring", + "A\ttext\nstring", + }, + // Handle simple named entities. + { + "simple", + "& > <", + "& > <", + }, + // Handle hitting the end of the string. + { + "stringEnd", + "& &", + "& &", + }, + // Handle entities with two codepoints. + { + "multiCodepoint", + "text ⋛︀ blah", + "text \u22db\ufe00 blah", + }, + // Handle decimal numeric entities. + { + "decimalEntity", + "Delta = Δ ", + "Delta = Δ ", + }, + // Handle hexadecimal numeric entities. + { + "hexadecimalEntity", + "Lambda = λ = λ ", + "Lambda = λ = λ ", + }, + // Handle numeric early termination. + { + "numericEnds", + "&# &#x €43 © = ©f = ©", + "&# &#x €43 © = ©f = ©", + }, + // Handle numeric ISO-8859-1 entity replacements. + { + "numericReplacements", + "Footnote‡", + "Footnote‡", + }, +} + +func TestUnescape(t *testing.T) { + for _, tt := range unescapeTests { + unescaped := UnescapeString(tt.html) + if unescaped != tt.unescaped { + t.Errorf("TestUnescape %s: want %q, got %q", tt.desc, tt.unescaped, unescaped) + } + } +} + +func TestUnescapeEscape(t *testing.T) { + ss := []string{ + ``, + `abc def`, + `a & b`, + `a&b`, + `a & b`, + `"`, + `"`, + `"<&>"`, + `"<&>"`, + `3&5==1 && 0<1, "0<1", a+acute=á`, + `The special characters are: <, >, &, ' and "`, + } + for _, s := range ss { + if got := UnescapeString(EscapeString(s)); got != s { + t.Errorf("got %q want %q", got, s) + } + } +} diff --git a/src/pkg/html/template/clone_test.go b/src/pkg/html/template/clone_test.go index 2663cddc2..e11bff2c5 100644 --- a/src/pkg/html/template/clone_test.go +++ b/src/pkg/html/template/clone_test.go @@ -6,6 +6,8 @@ package template import ( "bytes" + "errors" + "io/ioutil" "testing" "text/template/parse" ) @@ -146,3 +148,41 @@ func TestCloneCrash(t *testing.T) { Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`)) t1.Clone() } + +// Ensure that this guarantee from the docs is upheld: +// "Further calls to Parse in the copy will add templates +// to the copy but not to the original." +func TestCloneThenParse(t *testing.T) { + t0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`)) + t1 := Must(t0.Clone()) + Must(t1.Parse(`{{define "embedded"}}t1{{end}}`)) + if len(t0.Templates())+1 != len(t1.Templates()) { + t.Error("adding a template to a clone added it to the original") + } + // double check that the embedded template isn't available in the original + err := t0.ExecuteTemplate(ioutil.Discard, "a", nil) + if err == nil { + t.Error("expected 'no such template' error") + } +} + +// https://code.google.com/p/go/issues/detail?id=5980 +func TestFuncMapWorksAfterClone(t *testing.T) { + funcs := FuncMap{"customFunc": func() (string, error) { + return "", errors.New("issue5980") + }} + + // get the expected error output (no clone) + uncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) + wantErr := uncloned.Execute(ioutil.Discard, nil) + + // toClone must be the same as uncloned. It has to be recreated from scratch, + // since cloning cannot occur after execution. + toClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) + cloned := Must(toClone.Clone()) + gotErr := cloned.Execute(ioutil.Discard, nil) + + if wantErr.Error() != gotErr.Error() { + t.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr) + } +} diff --git a/src/pkg/html/template/content.go b/src/pkg/html/template/content.go index 9d1f74f6f..41b1116a6 100644 --- a/src/pkg/html/template/content.go +++ b/src/pkg/html/template/content.go @@ -74,6 +74,9 @@ const ( // indirect returns the value, after dereferencing as many times // as necessary to reach the base type (or nil). func indirect(a interface{}) interface{} { + if a == nil { + return nil + } if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { // Avoid creating a reflect.Value if it's not a pointer. return a @@ -94,6 +97,9 @@ var ( // as necessary to reach the base type (or nil) or an implementation of fmt.Stringer // or error, func indirectToStringerOrError(a interface{}) interface{} { + if a == nil { + return nil + } v := reflect.ValueOf(a) for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() { v = v.Elem() diff --git a/src/pkg/html/template/content_test.go b/src/pkg/html/template/content_test.go index 3c32e5e89..5f3ffe2d3 100644 --- a/src/pkg/html/template/content_test.go +++ b/src/pkg/html/template/content_test.go @@ -123,29 +123,29 @@ func TestTypedContent(t *testing.T) { { `<script>alert({{.}})</script>`, []string{ - `"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, + `"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`, `"a[href =~ \"//example.com\"]#foo"`, - `"Hello, \u003cb\u003eWorld\u003c/b\u003e &tc!"`, + `"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`, `" dir=\"ltr\""`, // Not escaped. `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, - `"greeting=H%69&addressee=(World)"`, + `"greeting=H%69\u0026addressee=(World)"`, }, }, { `<button onclick="alert({{.}})">`, []string{ - `"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, + `"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`, `"a[href =~ \"//example.com\"]#foo"`, - `"Hello, \u003cb\u003eWorld\u003c/b\u003e &amp;tc!"`, + `"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`, `" dir=\"ltr\""`, // Not JS escaped but HTML escaped. `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, - `"greeting=H%69&addressee=(World)"`, + `"greeting=H%69\u0026addressee=(World)"`, }, }, { @@ -259,3 +259,22 @@ func TestStringer(t *testing.T) { t.Errorf("expected %q got %q", expect, b.String()) } } + +// https://code.google.com/p/go/issues/detail?id=5982 +func TestEscapingNilNonemptyInterfaces(t *testing.T) { + tmpl := Must(New("x").Parse("{{.E}}")) + + got := new(bytes.Buffer) + testData := struct{ E error }{} // any non-empty interface here will do; error is just ready at hand + tmpl.Execute(got, testData) + + // Use this data instead of just hard-coding "<nil>" to avoid + // dependencies on the html escaper and the behavior of fmt w.r.t. nil. + want := new(bytes.Buffer) + data := struct{ E string }{E: fmt.Sprint(nil)} + tmpl.Execute(want, data) + + if !bytes.Equal(want.Bytes(), got.Bytes()) { + t.Errorf("expected %q got %q", string(want.Bytes()), string(got.Bytes())) + } +} diff --git a/src/pkg/html/template/context.go b/src/pkg/html/template/context.go index 7202221b8..eb47e2be3 100644 --- a/src/pkg/html/template/context.go +++ b/src/pkg/html/template/context.go @@ -29,7 +29,7 @@ func (c context) String() string { return fmt.Sprintf("{%v %v %v %v %v %v %v}", c.state, c.delim, c.urlPart, c.jsCtx, c.attr, c.element, c.err) } -// eq returns whether two contexts are equal. +// eq reports whether two contexts are equal. func (c context) eq(d context) bool { return c.state == d.state && c.delim == d.delim && diff --git a/src/pkg/html/template/css.go b/src/pkg/html/template/css.go index 3bcd98498..634f183f7 100644 --- a/src/pkg/html/template/css.go +++ b/src/pkg/html/template/css.go @@ -11,7 +11,7 @@ import ( "unicode/utf8" ) -// endsWithCSSKeyword returns whether b ends with an ident that +// endsWithCSSKeyword reports whether b ends with an ident that // case-insensitively matches the lower-case kw. func endsWithCSSKeyword(b []byte, kw string) bool { i := len(b) - len(kw) @@ -34,7 +34,7 @@ func endsWithCSSKeyword(b []byte, kw string) bool { return string(bytes.ToLower(b[i:])) == kw } -// isCSSNmchar returns whether rune is allowed anywhere in a CSS identifier. +// isCSSNmchar reports whether rune is allowed anywhere in a CSS identifier. func isCSSNmchar(r rune) bool { // Based on the CSS3 nmchar production but ignores multi-rune escape // sequences. @@ -99,7 +99,7 @@ func decodeCSS(s []byte) []byte { return b } -// isHex returns whether the given character is a hex digit. +// isHex reports whether the given character is a hex digit. func isHex(c byte) bool { return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' } @@ -144,7 +144,7 @@ func skipCSSSpace(c []byte) []byte { return c } -// isCSSSpace returns whether b is a CSS space char as defined in wc. +// isCSSSpace reports whether b is a CSS space char as defined in wc. func isCSSSpace(b byte) bool { switch b { case '\t', '\n', '\f', '\r', ' ': diff --git a/src/pkg/html/template/escape.go b/src/pkg/html/template/escape.go index 4829bfcc4..9ae9749db 100644 --- a/src/pkg/html/template/escape.go +++ b/src/pkg/html/template/escape.go @@ -35,11 +35,13 @@ func escapeTemplates(tmpl *Template, names ...string) error { for _, name := range names { if t := tmpl.set[name]; t != nil { t.text.Tree = nil + t.Tree = nil } } return err } tmpl.escaped = true + tmpl.Tree = tmpl.text.Tree } e.commit() return nil @@ -301,7 +303,7 @@ func indexOfStr(s string, strs []string, eq func(a, b string) bool) int { return -1 } -// escFnsEq returns whether the two escaping functions are equivalent. +// escFnsEq reports whether the two escaping functions are equivalent. func escFnsEq(a, b string) bool { if e := equivEscapers[a]; e != "" { a = e diff --git a/src/pkg/html/template/escape_test.go b/src/pkg/html/template/escape_test.go index de3659ba8..58383a6cd 100644 --- a/src/pkg/html/template/escape_test.go +++ b/src/pkg/html/template/escape_test.go @@ -538,7 +538,7 @@ func TestEscape(t *testing.T) { { "typed HTML in script", `<button onclick="alert({{.W}})">`, - `<button onclick="alert("&iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, + `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, }, { "typed HTML in RCDATA", @@ -655,6 +655,11 @@ func TestEscape(t *testing.T) { for _, test := range tests { tmpl := New(test.name) tmpl = Must(tmpl.Parse(test.input)) + // Check for bug 6459: Tree field was not set in Parse. + if tmpl.Tree != tmpl.text.Tree { + t.Errorf("%s: tree not set properly", test.name) + continue + } b := new(bytes.Buffer) if err := tmpl.Execute(b, data); err != nil { t.Errorf("%s: template execution failed: %s", test.name, err) @@ -673,6 +678,10 @@ func TestEscape(t *testing.T) { t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) continue } + if tmpl.Tree != tmpl.text.Tree { + t.Errorf("%s: tree mismatch", test.name) + continue + } } } diff --git a/src/pkg/html/template/js.go b/src/pkg/html/template/js.go index a9740931f..d594e0ad7 100644 --- a/src/pkg/html/template/js.go +++ b/src/pkg/html/template/js.go @@ -341,7 +341,7 @@ var jsRegexpReplacementTable = []string{ '}': `\}`, } -// isJSIdentPart returns whether the given rune is a JS identifier part. +// isJSIdentPart reports whether the given rune is a JS identifier part. // It does not handle all the non-Latin letters, joiners, and combining marks, // but it does handle every codepoint that can occur in a numeric literal or // a keyword. diff --git a/src/pkg/html/template/template.go b/src/pkg/html/template/template.go index e183898d5..11cc34a50 100644 --- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -21,7 +21,9 @@ type Template struct { // We could embed the text/template field, but it's safer not to because // we need to keep our version of the name space and the underlying // template's in sync. - text *template.Template + text *template.Template + // The underlying template's parse tree, updated to be HTML-safe. + Tree *parse.Tree *nameSpace // common to all associated templates } @@ -126,8 +128,10 @@ func (t *Template) Parse(src string) (*Template, error) { if tmpl == nil { tmpl = t.new(name) } + // Restore our record of this text/template to its unescaped original state. tmpl.escaped = false tmpl.text = v + tmpl.Tree = v.Tree } return t, nil } @@ -149,6 +153,7 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error ret := &Template{ false, text, + text.Tree, t.nameSpace, } t.set[name] = ret @@ -176,6 +181,7 @@ func (t *Template) Clone() (*Template, error) { ret := &Template{ false, textClone, + textClone.Tree, &nameSpace{ set: make(map[string]*Template), }, @@ -186,15 +192,11 @@ func (t *Template) Clone() (*Template, error) { if src == nil || src.escaped { return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name()) } - if x.Tree != nil { - x.Tree = &parse.Tree{ - Name: x.Tree.Name, - Root: x.Tree.Root.CopyList(), - } - } + x.Tree = x.Tree.Copy() ret.set[name] = &Template{ false, x, + x.Tree, ret.nameSpace, } } @@ -206,6 +208,7 @@ func New(name string) *Template { tmpl := &Template{ false, template.New(name), + nil, &nameSpace{ set: make(map[string]*Template), }, @@ -228,6 +231,7 @@ func (t *Template) new(name string) *Template { tmpl := &Template{ false, t.text.New(name), + nil, t.nameSpace, } tmpl.set[name] = tmpl diff --git a/src/pkg/html/template/transition.go b/src/pkg/html/template/transition.go index 564eb2020..7f30a7ab8 100644 --- a/src/pkg/html/template/transition.go +++ b/src/pkg/html/template/transition.go @@ -504,12 +504,12 @@ var elementNameMap = map[string]element{ "title": elementTitle, } -// asciiAlpha returns whether c is an ASCII letter. +// asciiAlpha reports whether c is an ASCII letter. func asciiAlpha(c byte) bool { return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' } -// asciiAlphaNum returns whether c is an ASCII letter or digit. +// asciiAlphaNum reports whether c is an ASCII letter or digit. func asciiAlphaNum(c byte) bool { return asciiAlpha(c) || '0' <= c && c <= '9' } |