| // 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" |
| "strings" |
| "template" |
| "template/parse" |
| "testing" |
| ) |
| |
| func TestEscape(t *testing.T) { |
| var data = struct { |
| F, T bool |
| C, G, H string |
| A, E []string |
| N int |
| Z *int |
| }{ |
| F: false, |
| T: true, |
| C: "<Cincinatti>", |
| G: "<Goodbye>", |
| H: "<Hello>", |
| A: []string{"<a>", "<b>"}, |
| E: []string{}, |
| N: 42, |
| Z: nil, |
| } |
| |
| tests := []struct { |
| name string |
| input string |
| output string |
| }{ |
| { |
| "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", |
| }, |
| { |
| // TODO: Make sure the URL escaper escapes single quotes so it can |
| // be embedded in single quoted URI attributes and CSS url(...) |
| // constructs. Single quotes are reserved in URLs, but are only used |
| // in the obsolete "mark" rule in an appendix in RFC 3986 so can be |
| // safely encoded. |
| "constant", |
| `<a href="/search?q={{"'a<b'"}}">`, |
| `<a href="/search?q='a%3Cb'">`, |
| }, |
| { |
| "multipleAttrs", |
| "<a b=1 c={{.H}}>", |
| "<a b=1 c=<Hello>>", |
| }, |
| { |
| "urlStartRel", |
| `<a href='{{"/foo/bar?a=b&c=d"}}'>`, |
| `<a href='/foo/bar?a=b&c=d'>`, |
| }, |
| { |
| "urlStartAbsOk", |
| `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, |
| `<a href='http://example.com/foo/bar?a=b&c=d'>`, |
| }, |
| { |
| "protocolRelativeURLStart", |
| `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`, |
| `<a href='//example.com:8000/foo/bar?a=b&c=d'>`, |
| }, |
| { |
| "pathRelativeURLStart", |
| `<a href="{{"/javascript:80/foo/bar"}}">`, |
| `<a href="/javascript:80/foo/bar">`, |
| }, |
| { |
| "dangerousURLStart", |
| `<a href='{{"javascript:alert(%22pwned%22)"}}'>`, |
| `<a href='#ZgotmplZ'>`, |
| }, |
| { |
| "urlPath", |
| `<a href='http://{{"javascript:80"}}/foo'>`, |
| `<a href='http://javascript:80/foo'>`, |
| }, |
| { |
| "urlQuery", |
| `<a href='/search?q={{.H}}'>`, |
| `<a href='/search?q=%3CHello%3E'>`, |
| }, |
| { |
| "urlFragment", |
| `<a href='/faq#{{.H}}'>`, |
| `<a href='/faq#%3CHello%3E'>`, |
| }, |
| { |
| "urlBranch", |
| `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, |
| `<a href="/bar">`, |
| }, |
| { |
| "urlBranchConflictMoot", |
| `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, |
| `<a href="/foo?a=%3CCincinatti%3E">`, |
| }, |
| { |
| "jsStrValue", |
| "<button onclick='alert({{.H}})'>", |
| `<button onclick='alert("\u003cHello\u003e")'>`, |
| }, |
| { |
| "jsNumericValue", |
| "<button onclick='alert({{.N}})'>", |
| `<button onclick='alert( 42 )'>`, |
| }, |
| { |
| "jsBoolValue", |
| "<button onclick='alert({{.T}})'>", |
| `<button onclick='alert( true )'>`, |
| }, |
| { |
| "jsNilValue", |
| "<button onclick='alert(typeof{{.Z}})'>", |
| `<button onclick='alert(typeof null )'>`, |
| }, |
| { |
| "jsObjValue", |
| "<button onclick='alert({{.A}})'>", |
| `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
| }, |
| { |
| "jsObjValueNotOverEscaped", |
| "<button onclick='alert({{.A | html}})'>", |
| `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
| }, |
| { |
| "jsStr", |
| "<button onclick='alert("{{.H}}")'>", |
| `<button onclick='alert("\x3cHello\x3e")'>`, |
| }, |
| { |
| "jsStrNotUnderEscaped", |
| "<button onclick='alert({{.C | urlquery}})'>", |
| // URL escaped, then quoted for JS. |
| `<button onclick='alert("%3CCincinatti%3E")'>`, |
| }, |
| { |
| "jsRe", |
| "<button onclick='alert("{{.H}}")'>", |
| `<button onclick='alert("\x3cHello\x3e")'>`, |
| }, |
| } |
| |
| for _, test := range tests { |
| tmpl := template.Must(template.New(test.name).Parse(test.input)) |
| tmpl, err := Escape(tmpl) |
| b := new(bytes.Buffer) |
| if err = tmpl.Execute(b, data); err != nil { |
| t.Errorf("%s: template execution failed: %s", test.name, 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.name, w, g) |
| continue |
| } |
| } |
| } |
| |
| func TestErrors(t *testing.T) { |
| tests := []struct { |
| input string |
| err string |
| }{ |
| // Non-error cases. |
| { |
| "{{if .Cond}}<a>{{else}}<b>{{end}}", |
| "", |
| }, |
| { |
| "{{if .Cond}}<a>{{end}}", |
| "", |
| }, |
| { |
| "{{if .Cond}}{{else}}<b>{{end}}", |
| "", |
| }, |
| { |
| "{{with .Cond}}<div>{{end}}", |
| "", |
| }, |
| { |
| "{{range .Items}}<a>{{end}}", |
| "", |
| }, |
| { |
| "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", |
| "", |
| }, |
| // Error cases. |
| { |
| "{{if .Cond}}<a{{end}}", |
| "z:1: {{if}} branches", |
| }, |
| { |
| "{{if .Cond}}\n{{else}}\n<a{{end}}", |
| "z:1: {{if}} branches", |
| }, |
| { |
| // Missing quote in the else branch. |
| `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, |
| "z:1: {{if}} branches", |
| }, |
| { |
| // Different kind of attribute: href implies a URL. |
| "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", |
| "z:1: {{if}} branches", |
| }, |
| { |
| "\n{{with .X}}<a{{end}}", |
| "z:2: {{with}} branches", |
| }, |
| { |
| "\n{{with .X}}<a>{{else}}<a{{end}}", |
| "z:2: {{with}} branches", |
| }, |
| { |
| "{{range .Items}}<a{{end}}", |
| `z:1: on range loop re-entry: "<" in attribute name: "<a"`, |
| }, |
| { |
| "\n{{range .Items}} x='<a{{end}}", |
| "z:2: on range loop re-entry: {{range}} branches", |
| }, |
| { |
| "<a b=1 c={{.H}}", |
| "z ends in a non-text context: {stateAttr delimSpaceOrTagEnd", |
| }, |
| { |
| `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, |
| "z:1: (action: [(command: [F=[H]])]) appears in an ambiguous URL context", |
| }, |
| { |
| `<a onclick="alert('Hello \`, |
| `unfinished escape sequence in JS string: "Hello \\"`, |
| }, |
| { |
| `<a onclick='alert("Hello\, World\`, |
| `unfinished escape sequence in JS string: "Hello\\, World\\"`, |
| }, |
| { |
| `<a onclick='alert(/x+\`, |
| `unfinished escape sequence in JS regexp: "x+\\"`, |
| }, |
| { |
| `<a onclick="/foo[\]/`, |
| `unfinished JS regexp charset: "foo[\\]/"`, |
| }, |
| { |
| `<a onclick="/* alert({{.X}} */">`, |
| `z:1: (action: [(command: [F=[X]])]) appears inside a comment`, |
| }, |
| { |
| `<a onclick="// alert({{.X}}">`, |
| `z:1: (action: [(command: [F=[X]])]) appears inside a comment`, |
| }, |
| } |
| |
| for _, test := range tests { |
| tmpl := template.Must(template.New("z").Parse(test.input)) |
| var got string |
| if _, err := Escape(tmpl); err != nil { |
| got = err.String() |
| } |
| if test.err == "" { |
| if got != "" { |
| t.Errorf("input=%q: unexpected error %q", test.input, got) |
| } |
| continue |
| } |
| if strings.Index(got, test.err) == -1 { |
| t.Errorf("input=%q: error %q does not contain expected string %q", test.input, got, test.err) |
| continue |
| } |
| } |
| } |
| |
| func TestEscapeText(t *testing.T) { |
| tests := []struct { |
| input string |
| output context |
| }{ |
| { |
| ``, |
| context{}, |
| }, |
| { |
| `Hello, World!`, |
| context{}, |
| }, |
| { |
| // An orphaned "<" is OK. |
| `I <3 Ponies!`, |
| context{}, |
| }, |
| { |
| `<a`, |
| context{state: stateTag}, |
| }, |
| { |
| `<a `, |
| context{state: stateTag}, |
| }, |
| { |
| `<a>`, |
| context{state: stateText}, |
| }, |
| { |
| `<a href=`, |
| context{state: stateURL, delim: delimSpaceOrTagEnd}, |
| }, |
| { |
| `<a href=x`, |
| context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href=x `, |
| context{state: stateTag}, |
| }, |
| { |
| `<a href=>`, |
| context{state: stateText}, |
| }, |
| { |
| `<a href=x>`, |
| context{state: stateText}, |
| }, |
| { |
| `<a href ='`, |
| context{state: stateURL, delim: delimSingleQuote}, |
| }, |
| { |
| `<a href=''`, |
| context{state: stateTag}, |
| }, |
| { |
| `<a href= "`, |
| context{state: stateURL, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a href=""`, |
| context{state: stateTag}, |
| }, |
| { |
| `<a title="`, |
| context{state: stateAttr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a HREF='http:`, |
| context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a Href='/`, |
| context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href='"`, |
| context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href="'`, |
| context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href=''`, |
| context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href=""`, |
| context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href=""`, |
| context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<a href="`, |
| context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, |
| }, |
| { |
| `<img alt="1">`, |
| context{state: stateText}, |
| }, |
| { |
| `<img alt="1>"`, |
| context{state: stateTag}, |
| }, |
| { |
| `<img alt="1>">`, |
| context{state: stateText}, |
| }, |
| { |
| `<input checked type="checkbox"`, |
| context{state: stateTag}, |
| }, |
| { |
| `<a onclick="`, |
| context{state: stateJS, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="//foo`, |
| context{state: stateJSLineCmt, delim: delimDoubleQuote}, |
| }, |
| { |
| "<a onclick='//\n", |
| context{state: stateJS, delim: delimSingleQuote}, |
| }, |
| { |
| "<a onclick='//\r\n", |
| context{state: stateJS, delim: delimSingleQuote}, |
| }, |
| { |
| "<a onclick='//\u2028", |
| context{state: stateJS, delim: delimSingleQuote}, |
| }, |
| { |
| `<a onclick="/*`, |
| context{state: stateJSBlockCmt, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onkeypress=""`, |
| context{state: stateJSDqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick='"foo"`, |
| context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick='foo'`, |
| context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick='foo`, |
| context{state: stateJSSqStr, delim: delimSpaceOrTagEnd}, |
| }, |
| { |
| `<a onclick=""foo'`, |
| context{state: stateJSDqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="'foo"`, |
| context{state: stateJSSqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<A ONCLICK="'`, |
| context{state: stateJSSqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="/`, |
| context{state: stateJSRegexp, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="'foo'`, |
| context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick="'foo\'`, |
| context{state: stateJSSqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="'foo\'`, |
| context{state: stateJSSqStr, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="/foo/`, |
| context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick="1 /foo`, |
| context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick="1 /*c*/ /foo`, |
| context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, |
| }, |
| { |
| `<a onclick="/foo[/]`, |
| context{state: stateJSRegexp, delim: delimDoubleQuote}, |
| }, |
| { |
| `<a onclick="/foo\/`, |
| context{state: stateJSRegexp, delim: delimDoubleQuote}, |
| }, |
| } |
| |
| for _, test := range tests { |
| b := []byte(test.input) |
| c := 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 |
| } |
| if test.input != string(b) { |
| t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) |
| continue |
| } |
| } |
| } |
| |
| func TestEnsurePipelineContains(t *testing.T) { |
| tests := []struct { |
| input, output string |
| ids []string |
| }{ |
| { |
| "{{.X}}", |
| "[(command: [F=[X]])]", |
| []string{}, |
| }, |
| { |
| "{{.X | html}}", |
| "[(command: [F=[X]]) (command: [I=html])]", |
| []string{}, |
| }, |
| { |
| "{{.X}}", |
| "[(command: [F=[X]]) (command: [I=html])]", |
| []string{"html"}, |
| }, |
| { |
| "{{.X | html}}", |
| "[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", |
| []string{"urlquery"}, |
| }, |
| { |
| "{{.X | html | urlquery}}", |
| "[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", |
| []string{"urlquery"}, |
| }, |
| { |
| "{{.X | html | urlquery}}", |
| "[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", |
| []string{"html", "urlquery"}, |
| }, |
| { |
| "{{.X | html | urlquery}}", |
| "[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", |
| []string{"html"}, |
| }, |
| { |
| "{{.X | urlquery}}", |
| "[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", |
| []string{"html", "urlquery"}, |
| }, |
| { |
| "{{.X | html | print}}", |
| "[(command: [F=[X]]) (command: [I=urlquery]) (command: [I=html]) (command: [I=print])]", |
| []string{"urlquery", "html"}, |
| }, |
| } |
| for _, 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("First node is not an action: %s", test.input) |
| continue |
| } |
| pipe := action.Pipe |
| ensurePipelineContains(pipe, test.ids) |
| got := pipe.String() |
| if got != test.output { |
| t.Errorf("%s, %v: want\n\t%s\ngot\n\t%s", test.input, test.ids, test.output, got) |
| } |
| } |
| } |