URL
https://opencores.org/ocsvn/openrisc/openrisc/trunk
Subversion Repositories openrisc
[/] [openrisc/] [trunk/] [gnu-dev/] [or1k-gcc/] [libgo/] [go/] [html/] [template/] [escape_test.go] - Rev 747
Compare with Previous | Blame | View Log
// 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 templateimport ("bytes""encoding/json""fmt""strings""testing""text/template""text/template/parse")type badMarshaler struct{}func (x *badMarshaler) MarshalJSON() ([]byte, error) {// Keys in valid JSON must be double quoted as must all strings.return []byte("{ foo: 'not quite valid JSON' }"), nil}type goodMarshaler struct{}func (x *goodMarshaler) MarshalJSON() ([]byte, error) {return []byte(`{ "<foo>": "O'Reilly" }`), nil}func TestEscape(t *testing.T) {data := struct {F, T boolC, G, H stringA, E []stringB, M json.MarshalerN intZ *intW HTML}{F: false,T: true,C: "<Cincinatti>",G: "<Goodbye>",H: "<Hello>",A: []string{"<a>", "<b>"},E: []string{},N: 42,B: &badMarshaler{},M: &goodMarshaler{},Z: nil,W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),}pdata := &datatests := []struct {name stringinput stringoutput string}{{"if","{{if .T}}Hello{{end}}, {{.C}}!","Hello, <Cincinatti>!",},{"else","{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!","<Goodbye>!",},{"overescaping1","Hello, {{.C | html}}!","Hello, <Cincinatti>!",},{"overescaping2","Hello, {{html .C}}!","Hello, <Cincinatti>!",},{"overescaping3","{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}","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="/search?q={{"'a<b'"}}">`,`<a href="/search?q=%27a%3cb%27">`,},{"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'>`,},{"dangerousURLStart2",`<a href=' {{"javascript:alert(%22pwned%22)"}}'>`,`<a href=' #ZgotmplZ'>`,},{"nonHierURL",`<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`,`<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`,},{"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"])'>`,},{"jsObjValueScript","<script>alert({{.A}})</script>",`<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`,},{"jsObjValueNotOverEscaped","<button onclick='alert({{.A | html}})'>",`<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`,},{"jsStr","<button onclick='alert("{{.H}}")'>",`<button onclick='alert("\x3cHello\x3e")'>`,},{"badMarshaller",`<button onclick='alert(1/{{.B}}in numbers)'>`,`<button onclick='alert(1/ /* json: error calling MarshalJSON for type *template.badMarshaler: invalid character 'f' looking for beginning of object key string */null in numbers)'>`,},{"jsMarshaller",`<button onclick='alert({{.M}})'>`,`<button onclick='alert({"<foo>":"O'Reilly"})'>`,},{"jsStrNotUnderEscaped","<button onclick='alert({{.C | urlquery}})'>",// URL escaped, then quoted for JS.`<button onclick='alert("%3CCincinatti%3E")'>`,},{"jsRe",`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,`<button onclick='alert(/foo\x2bbar/.test(""))'>`,},{"jsReBlank",`<script>alert(/{{""}}/.test(""));</script>`,`<script>alert(/(?:)/.test(""));</script>`,},{"jsReAmbigOk",`<script>{{if true}}var x = 1{{end}}</script>`,// The {if} ends in an ambiguous jsCtx but there is// no slash following so we shouldn't care.`<script>var x = 1</script>`,},{"styleBidiKeywordPassed",`<p style="dir: {{"ltr"}}">`,`<p style="dir: ltr">`,},{"styleBidiPropNamePassed",`<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`,`<p style="border-left: 0; border-right: 1in">`,},{"styleExpressionBlocked",`<p style="width: {{"expression(alert(1337))"}}">`,`<p style="width: ZgotmplZ">`,},{"styleTagSelectorPassed",`<style>{{"p"}} { color: pink }</style>`,`<style>p { color: pink }</style>`,},{"styleIDPassed",`<style>p{{"#my-ID"}} { font: Arial }</style>`,`<style>p#my-ID { font: Arial }</style>`,},{"styleClassPassed",`<style>p{{".my_class"}} { font: Arial }</style>`,`<style>p.my_class { font: Arial }</style>`,},{"styleQuantityPassed",`<a style="left: {{"2em"}}; top: {{0}}">`,`<a style="left: 2em; top: 0">`,},{"stylePctPassed",`<table style=width:{{"100%"}}>`,`<table style=width:100%>`,},{"styleColorPassed",`<p style="color: {{"#8ff"}}; background: {{"#000"}}">`,`<p style="color: #8ff; background: #000">`,},{"styleObfuscatedExpressionBlocked",`<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`,`<p style="width: ZgotmplZ">`,},{"styleMozBindingBlocked",`<p style="{{"-moz-binding(alert(1337))"}}: ...">`,`<p style="ZgotmplZ: ...">`,},{"styleObfuscatedMozBindingBlocked",`<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`,`<p style="ZgotmplZ: ...">`,},{"styleFontNameString",`<p style='font-family: "{{"Times New Roman"}}"'>`,`<p style='font-family: "Times New Roman"'>`,},{"styleFontNameString",`<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`,`<p style='font-family: "Times New Roman", "sans-serif"'>`,},{"styleFontNameUnquoted",`<p style='font-family: {{"Times New Roman"}}'>`,`<p style='font-family: Times New Roman'>`,},{"styleURLQueryEncoded",`<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`,`<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`,},{"styleQuotedURLQueryEncoded",`<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`,`<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`,},{"styleStrQueryEncoded",`<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`,`<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`,},{"styleURLBadProtocolBlocked",`<a style="background: url('{{"javascript:alert(1337)"}}')">`,`<a style="background: url('#ZgotmplZ')">`,},{"styleStrBadProtocolBlocked",`<a style="background: '{{"vbscript:alert(1337)"}}'">`,`<a style="background: '#ZgotmplZ'">`,},{"styleStrEncodedProtocolEncoded",`<a style="background: '{{"javascript\\3a alert(1337)"}}'">`,// The CSS string 'javascript\\3a alert(1337)' does not contains a colon.`<a style="background: 'javascript\\3a alert\28 1337\29 '">`,},{"styleURLGoodProtocolPassed",`<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`,`<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`,},{"styleStrGoodProtocolPassed",`<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`,`<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`,},{"styleURLEncodedForHTMLInAttr",`<a style="background: url('{{"/search?img=foo&size=icon"}}')">`,`<a style="background: url('/search?img=foo&size=icon')">`,},{"styleURLNotEncodedForHTMLInCdata",`<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`,`<style>body { background: url('/search?img=foo&size=icon') }</style>`,},{"styleURLMixedCase",`<p style="background: URL(#{{.H}})">`,`<p style="background: URL(#%3cHello%3e)">`,},{"stylePropertyPairPassed",`<a style='{{"color: red"}}'>`,`<a style='color: red'>`,},{"styleStrSpecialsEncoded",`<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`,`<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`,},{"styleURLSpecialsEncoded",`<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`,`<a style="border-image: url(/**/%27%22;://%20%5c), url("/**/%27%22;://%20%5c"), url('/**/%27%22;://%20%5c'), 'http://www.example.com/?q=%2f%2a%2a%2f%27%22%3b%3a%2f%2f%20%5c''">`,},{"HTML comment","<b>Hello, <!-- name of world -->{{.C}}</b>","<b>Hello, <Cincinatti></b>",},{"HTML comment not first < in text node.","<<!-- -->!--","<!--",},{"HTML normalization 1","a < b","a < b",},{"HTML normalization 2","a << b","a << b",},{"HTML normalization 3","a<<!-- --><!-- -->b","a<b",},{"HTML doctype not normalized","<!DOCTYPE html>Hello, World!","<!DOCTYPE html>Hello, World!",},{"No doctype injection",`<!{{"DOCTYPE"}}`,"<!DOCTYPE",},{"Split HTML comment","<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>","<b>Hello, <Cincinatti></b>",},{"JS line comment","<script>for (;;) { if (c()) break// foo not a label\n" +"foo({{.T}});}</script>","<script>for (;;) { if (c()) break\n" +"foo( true );}</script>",},{"JS multiline block comment","<script>for (;;) { if (c()) break/* foo not a label\n" +" */foo({{.T}});}</script>",// Newline separates break from call. If newline// removed, then break will consume label leaving// code invalid."<script>for (;;) { if (c()) break\n" +"foo( true );}</script>",},{"JS single-line block comment","<script>for (;;) {\n" +"if (c()) break/* foo a label */foo;" +"x({{.T}});}</script>",// Newline separates break from call. If newline// removed, then break will consume label leaving// code invalid."<script>for (;;) {\n" +"if (c()) break foo;" +"x( true );}</script>",},{"JS block comment flush with mathematical division","<script>var a/*b*//c\nd</script>","<script>var a /c\nd</script>",},{"JS mixed comments","<script>var a/*b*///c\nd</script>","<script>var a \nd</script>",},{"CSS comments","<style>p// paragraph\n" +`{border: 1px/* color */{{"#00f"}}}</style>`,"<style>p\n" +"{border: 1px #00f}</style>",},{"JS attr block comment",`<a onclick="f(""); /* alert({{.H}}) */">`,// Attribute comment tests should pass if the comments// are successfully elided.`<a onclick="f(""); /* alert() */">`,},{"JS attr line comment",`<a onclick="// alert({{.G}})">`,`<a onclick="// alert()">`,},{"CSS attr block comment",`<a style="/* color: {{.H}} */">`,`<a style="/* color: */">`,},{"CSS attr line comment",`<a style="// color: {{.G}}">`,`<a style="// color: ">`,},{"HTML substitution commented out","<p><!-- {{.H}} --></p>","<p></p>",},{"Comment ends flush with start","<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>","<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>",},{"typed HTML in text",`{{.W}}`,`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`,},{"typed HTML in attribute",`<div title="{{.W}}">`,`<div title="¡Hello, O'World!">`,},{"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!")">`,},{"typed HTML in RCDATA",`<textarea>{{.W}}</textarea>`,`<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`,},{"range in textarea","<textarea>{{range .A}}{{.}}{{end}}</textarea>","<textarea><a><b></textarea>",},{"auditable exemption from escaping","{{range .A}}{{. | noescape}}{{end}}","<a><b>",},{"No tag injection",`{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`,`10$<script src,evil.org/pwnd.js...`,},{"No comment injection",`<{{"!--"}}`,`<!--`,},{"No RCDATA end tag injection",`<textarea><{{"/textarea "}}...</textarea>`,`<textarea></textarea ...</textarea>`,},{"optional attrs",`<img class="{{"iconClass"}}"` +`{{if .T}} id="{{"<iconId>"}}"{{end}}` +// Double quotes inside if/else.` src=` +`{{if .T}}"?{{"<iconPath>"}}"` +`{{else}}"images/cleardot.gif"{{end}}` +// Missing space before title, but it is not a// part of the src attribute.`{{if .T}}title="{{"<title>"}}"{{end}}` +// Quotes outside if/else.` alt="` +`{{if .T}}{{"<alt>"}}` +`{{else}}{{if .F}}{{"<title>"}}{{end}}` +`{{end}}"` +`>`,`<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`,},{"conditional valueless attr name",`<input{{if .T}} checked{{end}} name=n>`,`<input checked name=n>`,},{"conditional dynamic valueless attr name 1",`<input{{if .T}} {{"checked"}}{{end}} name=n>`,`<input checked name=n>`,},{"conditional dynamic valueless attr name 2",`<input {{if .T}}{{"checked"}} {{end}}name=n>`,`<input checked name=n>`,},{"dynamic attribute name",`<img on{{"load"}}="alert({{"loaded"}})">`,// Treated as JS since quotes are inserted.`<img onload="alert("loaded")">`,},{"bad dynamic attribute name 1",// Allow checked, selected, disabled, but not JS or// CSS attributes.`<input {{"onchange"}}="{{"doEvil()"}}">`,`<input ZgotmplZ="doEvil()">`,},{"bad dynamic attribute name 2",`<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`,`<div ZgotmplZ="color: expression(alert(1337))">`,},{"bad dynamic attribute name 3",// Allow title or alt, but not a URL.`<img {{"src"}}="{{"javascript:doEvil()"}}">`,`<img ZgotmplZ="javascript:doEvil()">`,},{"bad dynamic attribute name 4",// Structure preservation requires values to associate// with a consistent attribute.`<input checked {{""}}="Whose value am I?">`,`<input checked ZgotmplZ="Whose value am I?">`,},{"dynamic element name",`<h{{3}}><table><t{{"head"}}>...</h{{3}}>`,`<h3><table><thead>...</h3>`,},{"bad dynamic element name",// Dynamic element names are typically used to switch// between (thead, tfoot, tbody), (ul, ol), (th, td),// and other replaceable sets.// We do not currently easily support (ul, ol).// If we do change to support that, this test should// catch failures to filter out special tag names which// would violate the structure preservation property --// if any special tag name could be substituted, then// the content could be raw text/RCDATA for some inputs// and regular HTML content for others.`<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`,`<script>doEvil()</script>`,},}for _, test := range tests {tmpl := New(test.name)// TODO: Move noescape into template/func.gotmpl.Funcs(FuncMap{"noescape": func(a ...interface{}) string {return fmt.Sprint(a...)},})tmpl = Must(tmpl.Parse(test.input))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}b.Reset()if err := tmpl.Execute(b, pdata); err != nil {t.Errorf("%s: template execution failed for pointer: %s", test.name, err)continue}if w, g := test.output, b.String(); w != g {t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)continue}}}func TestEscapeSet(t *testing.T) {type dataItem struct {Children []*dataItemX string}data := dataItem{Children: []*dataItem{{X: "foo"},{X: "<bar>"},{Children: []*dataItem{{X: "baz"},},},},}tests := []struct {inputs map[string]stringwant 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, <World>!`,},// 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 = "\u003ca\u003e"<b;'>`,},// A recursive template that ends in its start context.{map[string]string{"main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`,},`foo <bar> 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><bar></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><bar><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 <100></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 similar context.{map[string]string{"main": `<script>var x=[{{template "countdown" 4}}];</script>`,"countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`,},`<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</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">`,},*/}// pred is a template function that returns the predecessor of a// natural number for testing recursive templates.fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) {if len(a) == 1 {if i, _ := a[0].(int); i > 0 {return i - 1, nil}}return nil, fmt.Errorf("undefined pred(%v)", a)}}for _, test := range tests {source := ""for name, body := range test.inputs {source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body)}tmpl, err := New("root").Funcs(fns).Parse(source)if err != nil {t.Errorf("error parsing %q: %v", source, err)continue}var b bytes.Bufferif err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("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 stringerr 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",},{"<script>foo();","z: ends in a non-text context: {stateJS",},{`<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,"z:1: {{.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 string: "x+\\"`,},{`<a onclick="/foo[\]/`,`unfinished JS regexp charset: "foo[\\]/"`,},{// It is ambiguous whether 1.5 should be 1\.5 or 1.5.// Either `var x = 1/- 1.5 /i.test(x)`// where `i.test(x)` is a method call of reference i,// or `/-1\.5/i.test(x)` which is a method call on a// case insensitive regular expression.`<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,`'/' could start a division or regexp: "/-"`,},{`{{template "foo"}}`,"z:1: no such template \"foo\"",},{`<div{{template "y"}}>` +// Illegal starting in stateTag but not in stateText.`{{define "y"}} foo<b{{end}}`,`"<" in attribute name: " foo<b"`,},{`<script>reverseList = [{{template "t"}}]</script>` +// Missing " after recursive call.`{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`,`: cannot compute output context for template t$htmltemplate_stateJS_elementScript`,},{`<input type=button value=onclick=>`,`html/template:z: "=" in unquoted attr: "onclick="`,},{`<input type=button value= onclick=>`,`html/template:z: "=" in unquoted attr: "onclick="`,},{`<input type=button value= 1+1=2>`,`html/template:z: "=" in unquoted attr: "1+1=2"`,},{"<a class=`foo>","html/template:z: \"`\" in unquoted attr: \"`foo\"",},{`<a style=font:'Arial'>`,`html/template:z: "'" in unquoted attr: "font:'Arial'"`,},{`<a=foo>`,`: expected space, attr name, or end of tag, but got "=foo>"`,},}for _, test := range tests {buf := new(bytes.Buffer)tmpl, err := New("z").Parse(test.input)if err != nil {t.Errorf("input=%q: unexpected parse error %s\n", test.input, err)continue}err = tmpl.Execute(buf, nil)var got stringif err != nil {got = err.Error()}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\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err)continue}}}func TestEscapeText(t *testing.T) {tests := []struct {input stringoutput 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: stateAttrName, attr: attrURL},},{`<a on`,context{state: stateAttrName, attr: attrScript},},{`<a href `,context{state: stateAfterName, attr: attrURL},},{`<a style = `,context{state: stateBeforeValue, attr: attrStyle},},{`<a href=`,context{state: stateBeforeValue, attr: attrURL},},{`<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 onclick="/*/`,context{state: stateJSBlockCmt, delim: delimDoubleQuote},},{`<a onclick="/**/`,context{state: stateJS, 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},},{`<script>/foo/ /=`,context{state: stateJS, element: elementScript},},{`<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},},{`<a onclick="/foo/`,context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},},{`<input checked style="`,context{state: stateCSS, delim: delimDoubleQuote},},{`<a style="//`,context{state: stateCSSLineCmt, delim: delimDoubleQuote},},{`<a style="//</script>`,context{state: stateCSSLineCmt, delim: delimDoubleQuote},},{"<a style='//\n",context{state: stateCSS, delim: delimSingleQuote},},{"<a style='//\r",context{state: stateCSS, delim: delimSingleQuote},},{`<a style="/*`,context{state: stateCSSBlockCmt, delim: delimDoubleQuote},},{`<a style="/*/`,context{state: stateCSSBlockCmt, delim: delimDoubleQuote},},{`<a style="/**/`,context{state: stateCSS, delim: delimDoubleQuote},},{`<a style="background: '`,context{state: stateCSSSqStr, delim: delimDoubleQuote},},{`<a style="background: "`,context{state: stateCSSDqStr, delim: delimDoubleQuote},},{`<a style="background: '/foo?img=`,context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag},},{`<a style="background: '/`,context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url("/`,context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url('/`,context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url('/)`,context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url('/ `,context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url(/`,context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},},{`<a style="background: url( `,context{state: stateCSSURL, delim: delimDoubleQuote},},{`<a style="background: url( /image?name=`,context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag},},{`<a style="background: url(x)`,context{state: stateCSS, delim: delimDoubleQuote},},{`<a style="background: url('x'`,context{state: stateCSS, delim: delimDoubleQuote},},{`<a style="background: url( x `,context{state: stateCSS, delim: delimDoubleQuote},},{`<!-- foo`,context{state: stateHTMLCmt},},{`<!-->`,context{state: stateHTMLCmt},},{`<!--->`,context{state: stateHTMLCmt},},{`<!-- foo -->`,context{state: stateText},},{`<script`,context{state: stateTag, element: elementScript},},{`<script `,context{state: stateTag, element: elementScript},},{`<script src="foo.js" `,context{state: stateTag, element: elementScript},},{`<script src='foo.js' `,context{state: stateTag, element: elementScript},},{`<script type=text/javascript `,context{state: stateTag, element: elementScript},},{`<script>foo`,context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},},{`<script>foo</script>`,context{state: stateText},},{`<script>foo</script><!--`,context{state: stateHTMLCmt},},{`<script>document.write("<p>foo</p>");`,context{state: stateJS, element: elementScript},},{`<script>document.write("<p>foo<\/script>");`,context{state: stateJS, element: elementScript},},{`<script>document.write("<script>alert(1)</script>");`,context{state: stateText},},{`<Script>`,context{state: stateJS, element: elementScript},},{`<SCRIPT>foo`,context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},},{`<textarea>value`,context{state: stateRCDATA, element: elementTextarea},},{`<textarea>value</TEXTAREA>`,context{state: stateText},},{`<textarea name=html><b`,context{state: stateRCDATA, element: elementTextarea},},{`<title>value`,context{state: stateRCDATA, element: elementTitle},},{`<style>value`,context{state: stateCSS, element: elementStyle},},{`<a xlink:href`,context{state: stateAttrName, attr: attrURL},},{`<a xmlns`,context{state: stateAttrName, attr: attrURL},},{`<a xmlns:foo`,context{state: stateAttrName, attr: attrURL},},{`<a xmlnsxyz`,context{state: stateAttrName},},{`<a data-url`,context{state: stateAttrName, attr: attrURL},},{`<a data-iconUri`,context{state: stateAttrName, attr: attrURL},},{`<a data-urlItem`,context{state: stateAttrName, attr: attrURL},},{`<a g:`,context{state: stateAttrName},},{`<a g:url`,context{state: stateAttrName, attr: attrURL},},{`<a g:iconUri`,context{state: stateAttrName, attr: attrURL},},{`<a g:urlItem`,context{state: stateAttrName, attr: attrURL},},{`<a g:value`,context{state: stateAttrName},},{`<a svg:style='`,context{state: stateCSS, delim: delimSingleQuote},},{`<svg:font-face`,context{state: stateTag},},{`<svg:a svg:onclick="`,context{state: stateJS, delim: delimDoubleQuote},},}for _, test := range tests {b, e := []byte(test.input), newEscaper(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)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 stringids []string}{{"{{.X}}",".X",[]string{},},{"{{.X | html}}",".X | html",[]string{},},{"{{.X}}",".X | html",[]string{"html"},},{"{{.X | html}}",".X | html | urlquery",[]string{"urlquery"},},{"{{.X | html | urlquery}}",".X | html | urlquery",[]string{"urlquery"},},{"{{.X | html | urlquery}}",".X | html | urlquery",[]string{"html", "urlquery"},},{"{{.X | html | urlquery}}",".X | html | urlquery",[]string{"html"},},{"{{.X | urlquery}}",".X | html | urlquery",[]string{"html", "urlquery"},},{"{{.X | html | print}}",".X | urlquery | html | print",[]string{"urlquery", "html"},},}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)continue}pipe := action.PipeensurePipelineContains(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)}}}func TestEscapeErrorsNotIgnorable(t *testing.T) {var b bytes.Buffertmpl, _ := New("dangerous").Parse("<a")err := tmpl.Execute(&b, nil)if err == nil {t.Errorf("Expected error")} else if b.Len() != 0 {t.Errorf("Emitted output despite escaping failure")}}func TestEscapeSetErrorsNotIgnorable(t *testing.T) {var b bytes.Buffertmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`)if err != nil {t.Errorf("failed to parse set: %q", err)}err = tmpl.ExecuteTemplate(&b, "t", nil)if err == nil {t.Errorf("Expected error")} else if b.Len() != 0 {t.Errorf("Emitted output despite escaping failure")}}func TestRedundantFuncs(t *testing.T) {inputs := []interface{}{"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +` !"#$%&'()*+,-./` +`0123456789:;<=>?` +`@ABCDEFGHIJKLMNO` +`PQRSTUVWXYZ[\]^_` +"`abcdefghijklmno" +"pqrstuvwxyz{|}~\x7f" +"\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" +"&%22\\",CSS(`a[href =~ "//example.com"]#foo`),HTML(`Hello, <b>World</b> &tc!`),HTMLAttr(` dir="ltr"`),JS(`c && alert("Hello, World!");`),JSStr(`Hello, World & O'Reilly\x21`),URL(`greeting=H%69&addressee=(World)`),}for n0, m := range redundantFuncs {f0 := funcMap[n0].(func(...interface{}) string)for n1 := range m {f1 := funcMap[n1].(func(...interface{}) string)for _, input := range inputs {want := f0(input)if got := f1(want); want != got {t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got)}}}}}func TestIndirectPrint(t *testing.T) {a := 3ap := &ab := "hello"bp := &bbpp := &bptmpl := Must(New("t").Parse(`{{.}}`))var buf bytes.Buffererr := tmpl.Execute(&buf, ap)if err != nil {t.Errorf("Unexpected error: %s", err)} else if buf.String() != "3" {t.Errorf(`Expected "3"; got %q`, buf.String())}buf.Reset()err = tmpl.Execute(&buf, bpp)if err != nil {t.Errorf("Unexpected error: %s", err)} else if buf.String() != "hello" {t.Errorf(`Expected "hello"; got %q`, buf.String())}}func BenchmarkEscapedExecute(b *testing.B) {tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`))var buf bytes.Bufferb.ResetTimer()for i := 0; i < b.N; i++ {tmpl.Execute(&buf, "foo & 'bar' & baz")buf.Reset()}}
