github.com/varialus/godfly@v0.0.0-20130904042352-1934f9f095ab/src/pkg/html/template/escape_test.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package template 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "os" 12 "strings" 13 "testing" 14 "text/template" 15 "text/template/parse" 16 ) 17 18 type badMarshaler struct{} 19 20 func (x *badMarshaler) MarshalJSON() ([]byte, error) { 21 // Keys in valid JSON must be double quoted as must all strings. 22 return []byte("{ foo: 'not quite valid JSON' }"), nil 23 } 24 25 type goodMarshaler struct{} 26 27 func (x *goodMarshaler) MarshalJSON() ([]byte, error) { 28 return []byte(`{ "<foo>": "O'Reilly" }`), nil 29 } 30 31 func TestEscape(t *testing.T) { 32 data := struct { 33 F, T bool 34 C, G, H string 35 A, E []string 36 B, M json.Marshaler 37 N int 38 Z *int 39 W HTML 40 }{ 41 F: false, 42 T: true, 43 C: "<Cincinatti>", 44 G: "<Goodbye>", 45 H: "<Hello>", 46 A: []string{"<a>", "<b>"}, 47 E: []string{}, 48 N: 42, 49 B: &badMarshaler{}, 50 M: &goodMarshaler{}, 51 Z: nil, 52 W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), 53 } 54 pdata := &data 55 56 tests := []struct { 57 name string 58 input string 59 output string 60 }{ 61 { 62 "if", 63 "{{if .T}}Hello{{end}}, {{.C}}!", 64 "Hello, <Cincinatti>!", 65 }, 66 { 67 "else", 68 "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", 69 "<Goodbye>!", 70 }, 71 { 72 "overescaping1", 73 "Hello, {{.C | html}}!", 74 "Hello, <Cincinatti>!", 75 }, 76 { 77 "overescaping2", 78 "Hello, {{html .C}}!", 79 "Hello, <Cincinatti>!", 80 }, 81 { 82 "overescaping3", 83 "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", 84 "Hello, <Cincinatti>!", 85 }, 86 { 87 "assignment", 88 "{{if $x := .H}}{{$x}}{{end}}", 89 "<Hello>", 90 }, 91 { 92 "withBody", 93 "{{with .H}}{{.}}{{end}}", 94 "<Hello>", 95 }, 96 { 97 "withElse", 98 "{{with .E}}{{.}}{{else}}{{.H}}{{end}}", 99 "<Hello>", 100 }, 101 { 102 "rangeBody", 103 "{{range .A}}{{.}}{{end}}", 104 "<a><b>", 105 }, 106 { 107 "rangeElse", 108 "{{range .E}}{{.}}{{else}}{{.H}}{{end}}", 109 "<Hello>", 110 }, 111 { 112 "nonStringValue", 113 "{{.T}}", 114 "true", 115 }, 116 { 117 "constant", 118 `<a href="/search?q={{"'a<b'"}}">`, 119 `<a href="/search?q=%27a%3cb%27">`, 120 }, 121 { 122 "multipleAttrs", 123 "<a b=1 c={{.H}}>", 124 "<a b=1 c=<Hello>>", 125 }, 126 { 127 "urlStartRel", 128 `<a href='{{"/foo/bar?a=b&c=d"}}'>`, 129 `<a href='/foo/bar?a=b&c=d'>`, 130 }, 131 { 132 "urlStartAbsOk", 133 `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, 134 `<a href='http://example.com/foo/bar?a=b&c=d'>`, 135 }, 136 { 137 "protocolRelativeURLStart", 138 `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`, 139 `<a href='//example.com:8000/foo/bar?a=b&c=d'>`, 140 }, 141 { 142 "pathRelativeURLStart", 143 `<a href="{{"/javascript:80/foo/bar"}}">`, 144 `<a href="/javascript:80/foo/bar">`, 145 }, 146 { 147 "dangerousURLStart", 148 `<a href='{{"javascript:alert(%22pwned%22)"}}'>`, 149 `<a href='#ZgotmplZ'>`, 150 }, 151 { 152 "dangerousURLStart2", 153 `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`, 154 `<a href=' #ZgotmplZ'>`, 155 }, 156 { 157 "nonHierURL", 158 `<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`, 159 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`, 160 }, 161 { 162 "urlPath", 163 `<a href='http://{{"javascript:80"}}/foo'>`, 164 `<a href='http://javascript:80/foo'>`, 165 }, 166 { 167 "urlQuery", 168 `<a href='/search?q={{.H}}'>`, 169 `<a href='/search?q=%3cHello%3e'>`, 170 }, 171 { 172 "urlFragment", 173 `<a href='/faq#{{.H}}'>`, 174 `<a href='/faq#%3cHello%3e'>`, 175 }, 176 { 177 "urlBranch", 178 `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, 179 `<a href="/bar">`, 180 }, 181 { 182 "urlBranchConflictMoot", 183 `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, 184 `<a href="/foo?a=%3cCincinatti%3e">`, 185 }, 186 { 187 "jsStrValue", 188 "<button onclick='alert({{.H}})'>", 189 `<button onclick='alert("\u003cHello\u003e")'>`, 190 }, 191 { 192 "jsNumericValue", 193 "<button onclick='alert({{.N}})'>", 194 `<button onclick='alert( 42 )'>`, 195 }, 196 { 197 "jsBoolValue", 198 "<button onclick='alert({{.T}})'>", 199 `<button onclick='alert( true )'>`, 200 }, 201 { 202 "jsNilValue", 203 "<button onclick='alert(typeof{{.Z}})'>", 204 `<button onclick='alert(typeof null )'>`, 205 }, 206 { 207 "jsObjValue", 208 "<button onclick='alert({{.A}})'>", 209 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 210 }, 211 { 212 "jsObjValueScript", 213 "<script>alert({{.A}})</script>", 214 `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`, 215 }, 216 { 217 "jsObjValueNotOverEscaped", 218 "<button onclick='alert({{.A | html}})'>", 219 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 220 }, 221 { 222 "jsStr", 223 "<button onclick='alert("{{.H}}")'>", 224 `<button onclick='alert("\x3cHello\x3e")'>`, 225 }, 226 { 227 "badMarshaler", 228 `<button onclick='alert(1/{{.B}}in numbers)'>`, 229 `<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)'>`, 230 }, 231 { 232 "jsMarshaler", 233 `<button onclick='alert({{.M}})'>`, 234 `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`, 235 }, 236 { 237 "jsStrNotUnderEscaped", 238 "<button onclick='alert({{.C | urlquery}})'>", 239 // URL escaped, then quoted for JS. 240 `<button onclick='alert("%3CCincinatti%3E")'>`, 241 }, 242 { 243 "jsRe", 244 `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, 245 `<button onclick='alert(/foo\x2bbar/.test(""))'>`, 246 }, 247 { 248 "jsReBlank", 249 `<script>alert(/{{""}}/.test(""));</script>`, 250 `<script>alert(/(?:)/.test(""));</script>`, 251 }, 252 { 253 "jsReAmbigOk", 254 `<script>{{if true}}var x = 1{{end}}</script>`, 255 // The {if} ends in an ambiguous jsCtx but there is 256 // no slash following so we shouldn't care. 257 `<script>var x = 1</script>`, 258 }, 259 { 260 "styleBidiKeywordPassed", 261 `<p style="dir: {{"ltr"}}">`, 262 `<p style="dir: ltr">`, 263 }, 264 { 265 "styleBidiPropNamePassed", 266 `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`, 267 `<p style="border-left: 0; border-right: 1in">`, 268 }, 269 { 270 "styleExpressionBlocked", 271 `<p style="width: {{"expression(alert(1337))"}}">`, 272 `<p style="width: ZgotmplZ">`, 273 }, 274 { 275 "styleTagSelectorPassed", 276 `<style>{{"p"}} { color: pink }</style>`, 277 `<style>p { color: pink }</style>`, 278 }, 279 { 280 "styleIDPassed", 281 `<style>p{{"#my-ID"}} { font: Arial }</style>`, 282 `<style>p#my-ID { font: Arial }</style>`, 283 }, 284 { 285 "styleClassPassed", 286 `<style>p{{".my_class"}} { font: Arial }</style>`, 287 `<style>p.my_class { font: Arial }</style>`, 288 }, 289 { 290 "styleQuantityPassed", 291 `<a style="left: {{"2em"}}; top: {{0}}">`, 292 `<a style="left: 2em; top: 0">`, 293 }, 294 { 295 "stylePctPassed", 296 `<table style=width:{{"100%"}}>`, 297 `<table style=width:100%>`, 298 }, 299 { 300 "styleColorPassed", 301 `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`, 302 `<p style="color: #8ff; background: #000">`, 303 }, 304 { 305 "styleObfuscatedExpressionBlocked", 306 `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`, 307 `<p style="width: ZgotmplZ">`, 308 }, 309 { 310 "styleMozBindingBlocked", 311 `<p style="{{"-moz-binding(alert(1337))"}}: ...">`, 312 `<p style="ZgotmplZ: ...">`, 313 }, 314 { 315 "styleObfuscatedMozBindingBlocked", 316 `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`, 317 `<p style="ZgotmplZ: ...">`, 318 }, 319 { 320 "styleFontNameString", 321 `<p style='font-family: "{{"Times New Roman"}}"'>`, 322 `<p style='font-family: "Times New Roman"'>`, 323 }, 324 { 325 "styleFontNameString", 326 `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`, 327 `<p style='font-family: "Times New Roman", "sans-serif"'>`, 328 }, 329 { 330 "styleFontNameUnquoted", 331 `<p style='font-family: {{"Times New Roman"}}'>`, 332 `<p style='font-family: Times New Roman'>`, 333 }, 334 { 335 "styleURLQueryEncoded", 336 `<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`, 337 `<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`, 338 }, 339 { 340 "styleQuotedURLQueryEncoded", 341 `<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`, 342 `<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`, 343 }, 344 { 345 "styleStrQueryEncoded", 346 `<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`, 347 `<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`, 348 }, 349 { 350 "styleURLBadProtocolBlocked", 351 `<a style="background: url('{{"javascript:alert(1337)"}}')">`, 352 `<a style="background: url('#ZgotmplZ')">`, 353 }, 354 { 355 "styleStrBadProtocolBlocked", 356 `<a style="background: '{{"vbscript:alert(1337)"}}'">`, 357 `<a style="background: '#ZgotmplZ'">`, 358 }, 359 { 360 "styleStrEncodedProtocolEncoded", 361 `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`, 362 // The CSS string 'javascript\\3a alert(1337)' does not contains a colon. 363 `<a style="background: 'javascript\\3a alert\28 1337\29 '">`, 364 }, 365 { 366 "styleURLGoodProtocolPassed", 367 `<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`, 368 `<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`, 369 }, 370 { 371 "styleStrGoodProtocolPassed", 372 `<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`, 373 `<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`, 374 }, 375 { 376 "styleURLEncodedForHTMLInAttr", 377 `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`, 378 `<a style="background: url('/search?img=foo&size=icon')">`, 379 }, 380 { 381 "styleURLNotEncodedForHTMLInCdata", 382 `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`, 383 `<style>body { background: url('/search?img=foo&size=icon') }</style>`, 384 }, 385 { 386 "styleURLMixedCase", 387 `<p style="background: URL(#{{.H}})">`, 388 `<p style="background: URL(#%3cHello%3e)">`, 389 }, 390 { 391 "stylePropertyPairPassed", 392 `<a style='{{"color: red"}}'>`, 393 `<a style='color: red'>`, 394 }, 395 { 396 "styleStrSpecialsEncoded", 397 `<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`, 398 `<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`, 399 }, 400 { 401 "styleURLSpecialsEncoded", 402 `<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`, 403 `<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''">`, 404 }, 405 { 406 "HTML comment", 407 "<b>Hello, <!-- name of world -->{{.C}}</b>", 408 "<b>Hello, <Cincinatti></b>", 409 }, 410 { 411 "HTML comment not first < in text node.", 412 "<<!-- -->!--", 413 "<!--", 414 }, 415 { 416 "HTML normalization 1", 417 "a < b", 418 "a < b", 419 }, 420 { 421 "HTML normalization 2", 422 "a << b", 423 "a << b", 424 }, 425 { 426 "HTML normalization 3", 427 "a<<!-- --><!-- -->b", 428 "a<b", 429 }, 430 { 431 "HTML doctype not normalized", 432 "<!DOCTYPE html>Hello, World!", 433 "<!DOCTYPE html>Hello, World!", 434 }, 435 { 436 "HTML doctype not case-insensitive", 437 "<!doCtYPE htMl>Hello, World!", 438 "<!doCtYPE htMl>Hello, World!", 439 }, 440 { 441 "No doctype injection", 442 `<!{{"DOCTYPE"}}`, 443 "<!DOCTYPE", 444 }, 445 { 446 "Split HTML comment", 447 "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>", 448 "<b>Hello, <Cincinatti></b>", 449 }, 450 { 451 "JS line comment", 452 "<script>for (;;) { if (c()) break// foo not a label\n" + 453 "foo({{.T}});}</script>", 454 "<script>for (;;) { if (c()) break\n" + 455 "foo( true );}</script>", 456 }, 457 { 458 "JS multiline block comment", 459 "<script>for (;;) { if (c()) break/* foo not a label\n" + 460 " */foo({{.T}});}</script>", 461 // Newline separates break from call. If newline 462 // removed, then break will consume label leaving 463 // code invalid. 464 "<script>for (;;) { if (c()) break\n" + 465 "foo( true );}</script>", 466 }, 467 { 468 "JS single-line block comment", 469 "<script>for (;;) {\n" + 470 "if (c()) break/* foo a label */foo;" + 471 "x({{.T}});}</script>", 472 // Newline separates break from call. If newline 473 // removed, then break will consume label leaving 474 // code invalid. 475 "<script>for (;;) {\n" + 476 "if (c()) break foo;" + 477 "x( true );}</script>", 478 }, 479 { 480 "JS block comment flush with mathematical division", 481 "<script>var a/*b*//c\nd</script>", 482 "<script>var a /c\nd</script>", 483 }, 484 { 485 "JS mixed comments", 486 "<script>var a/*b*///c\nd</script>", 487 "<script>var a \nd</script>", 488 }, 489 { 490 "CSS comments", 491 "<style>p// paragraph\n" + 492 `{border: 1px/* color */{{"#00f"}}}</style>`, 493 "<style>p\n" + 494 "{border: 1px #00f}</style>", 495 }, 496 { 497 "JS attr block comment", 498 `<a onclick="f(""); /* alert({{.H}}) */">`, 499 // Attribute comment tests should pass if the comments 500 // are successfully elided. 501 `<a onclick="f(""); /* alert() */">`, 502 }, 503 { 504 "JS attr line comment", 505 `<a onclick="// alert({{.G}})">`, 506 `<a onclick="// alert()">`, 507 }, 508 { 509 "CSS attr block comment", 510 `<a style="/* color: {{.H}} */">`, 511 `<a style="/* color: */">`, 512 }, 513 { 514 "CSS attr line comment", 515 `<a style="// color: {{.G}}">`, 516 `<a style="// color: ">`, 517 }, 518 { 519 "HTML substitution commented out", 520 "<p><!-- {{.H}} --></p>", 521 "<p></p>", 522 }, 523 { 524 "Comment ends flush with start", 525 "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>", 526 "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>", 527 }, 528 { 529 "typed HTML in text", 530 `{{.W}}`, 531 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, 532 }, 533 { 534 "typed HTML in attribute", 535 `<div title="{{.W}}">`, 536 `<div title="¡Hello, O'World!">`, 537 }, 538 { 539 "typed HTML in script", 540 `<button onclick="alert({{.W}})">`, 541 `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, 542 }, 543 { 544 "typed HTML in RCDATA", 545 `<textarea>{{.W}}</textarea>`, 546 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, 547 }, 548 { 549 "range in textarea", 550 "<textarea>{{range .A}}{{.}}{{end}}</textarea>", 551 "<textarea><a><b></textarea>", 552 }, 553 { 554 "No tag injection", 555 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, 556 `10$<script src,evil.org/pwnd.js...`, 557 }, 558 { 559 "No comment injection", 560 `<{{"!--"}}`, 561 `<!--`, 562 }, 563 { 564 "No RCDATA end tag injection", 565 `<textarea><{{"/textarea "}}...</textarea>`, 566 `<textarea></textarea ...</textarea>`, 567 }, 568 { 569 "optional attrs", 570 `<img class="{{"iconClass"}}"` + 571 `{{if .T}} id="{{"<iconId>"}}"{{end}}` + 572 // Double quotes inside if/else. 573 ` src=` + 574 `{{if .T}}"?{{"<iconPath>"}}"` + 575 `{{else}}"images/cleardot.gif"{{end}}` + 576 // Missing space before title, but it is not a 577 // part of the src attribute. 578 `{{if .T}}title="{{"<title>"}}"{{end}}` + 579 // Quotes outside if/else. 580 ` alt="` + 581 `{{if .T}}{{"<alt>"}}` + 582 `{{else}}{{if .F}}{{"<title>"}}{{end}}` + 583 `{{end}}"` + 584 `>`, 585 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, 586 }, 587 { 588 "conditional valueless attr name", 589 `<input{{if .T}} checked{{end}} name=n>`, 590 `<input checked name=n>`, 591 }, 592 { 593 "conditional dynamic valueless attr name 1", 594 `<input{{if .T}} {{"checked"}}{{end}} name=n>`, 595 `<input checked name=n>`, 596 }, 597 { 598 "conditional dynamic valueless attr name 2", 599 `<input {{if .T}}{{"checked"}} {{end}}name=n>`, 600 `<input checked name=n>`, 601 }, 602 { 603 "dynamic attribute name", 604 `<img on{{"load"}}="alert({{"loaded"}})">`, 605 // Treated as JS since quotes are inserted. 606 `<img onload="alert("loaded")">`, 607 }, 608 { 609 "bad dynamic attribute name 1", 610 // Allow checked, selected, disabled, but not JS or 611 // CSS attributes. 612 `<input {{"onchange"}}="{{"doEvil()"}}">`, 613 `<input ZgotmplZ="doEvil()">`, 614 }, 615 { 616 "bad dynamic attribute name 2", 617 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, 618 `<div ZgotmplZ="color: expression(alert(1337))">`, 619 }, 620 { 621 "bad dynamic attribute name 3", 622 // Allow title or alt, but not a URL. 623 `<img {{"src"}}="{{"javascript:doEvil()"}}">`, 624 `<img ZgotmplZ="javascript:doEvil()">`, 625 }, 626 { 627 "bad dynamic attribute name 4", 628 // Structure preservation requires values to associate 629 // with a consistent attribute. 630 `<input checked {{""}}="Whose value am I?">`, 631 `<input checked ZgotmplZ="Whose value am I?">`, 632 }, 633 { 634 "dynamic element name", 635 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, 636 `<h3><table><thead>...</h3>`, 637 }, 638 { 639 "bad dynamic element name", 640 // Dynamic element names are typically used to switch 641 // between (thead, tfoot, tbody), (ul, ol), (th, td), 642 // and other replaceable sets. 643 // We do not currently easily support (ul, ol). 644 // If we do change to support that, this test should 645 // catch failures to filter out special tag names which 646 // would violate the structure preservation property -- 647 // if any special tag name could be substituted, then 648 // the content could be raw text/RCDATA for some inputs 649 // and regular HTML content for others. 650 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, 651 `<script>doEvil()</script>`, 652 }, 653 } 654 655 for _, test := range tests { 656 tmpl := New(test.name) 657 tmpl = Must(tmpl.Parse(test.input)) 658 b := new(bytes.Buffer) 659 if err := tmpl.Execute(b, data); err != nil { 660 t.Errorf("%s: template execution failed: %s", test.name, err) 661 continue 662 } 663 if w, g := test.output, b.String(); w != g { 664 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) 665 continue 666 } 667 b.Reset() 668 if err := tmpl.Execute(b, pdata); err != nil { 669 t.Errorf("%s: template execution failed for pointer: %s", test.name, err) 670 continue 671 } 672 if w, g := test.output, b.String(); w != g { 673 t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) 674 continue 675 } 676 } 677 } 678 679 func TestEscapeSet(t *testing.T) { 680 type dataItem struct { 681 Children []*dataItem 682 X string 683 } 684 685 data := dataItem{ 686 Children: []*dataItem{ 687 {X: "foo"}, 688 {X: "<bar>"}, 689 { 690 Children: []*dataItem{ 691 {X: "baz"}, 692 }, 693 }, 694 }, 695 } 696 697 tests := []struct { 698 inputs map[string]string 699 want string 700 }{ 701 // The trivial set. 702 { 703 map[string]string{ 704 "main": ``, 705 }, 706 ``, 707 }, 708 // A template called in the start context. 709 { 710 map[string]string{ 711 "main": `Hello, {{template "helper"}}!`, 712 // Not a valid top level HTML template. 713 // "<b" is not a full tag. 714 "helper": `{{"<World>"}}`, 715 }, 716 `Hello, <World>!`, 717 }, 718 // A template called in a context other than the start. 719 { 720 map[string]string{ 721 "main": `<a onclick='a = {{template "helper"}};'>`, 722 // Not a valid top level HTML template. 723 // "<b" is not a full tag. 724 "helper": `{{"<a>"}}<b`, 725 }, 726 `<a onclick='a = "\u003ca\u003e"<b;'>`, 727 }, 728 // A recursive template that ends in its start context. 729 { 730 map[string]string{ 731 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 732 }, 733 `foo <bar> baz `, 734 }, 735 // A recursive helper template that ends in its start context. 736 { 737 map[string]string{ 738 "main": `{{template "helper" .}}`, 739 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 740 }, 741 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 742 }, 743 // Co-recursive templates that end in its start context. 744 { 745 map[string]string{ 746 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 747 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 748 }, 749 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 750 }, 751 // A template that is called in two different contexts. 752 { 753 map[string]string{ 754 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 755 "helper": `{{11}} of {{"<100>"}}`, 756 }, 757 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`, 758 }, 759 // A non-recursive template that ends in a different context. 760 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 761 { 762 map[string]string{ 763 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 764 "helper": "{{126}}", 765 }, 766 `<script>var x= 126 /"42";</script>`, 767 }, 768 // A recursive template that ends in a similar context. 769 { 770 map[string]string{ 771 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 772 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 773 }, 774 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 775 }, 776 // A recursive template that ends in a different context. 777 /* 778 { 779 map[string]string{ 780 "main": `<a href="/foo{{template "helper" .}}">`, 781 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 782 }, 783 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 784 }, 785 */ 786 } 787 788 // pred is a template function that returns the predecessor of a 789 // natural number for testing recursive templates. 790 fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) { 791 if len(a) == 1 { 792 if i, _ := a[0].(int); i > 0 { 793 return i - 1, nil 794 } 795 } 796 return nil, fmt.Errorf("undefined pred(%v)", a) 797 }} 798 799 for _, test := range tests { 800 source := "" 801 for name, body := range test.inputs { 802 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 803 } 804 tmpl, err := New("root").Funcs(fns).Parse(source) 805 if err != nil { 806 t.Errorf("error parsing %q: %v", source, err) 807 continue 808 } 809 var b bytes.Buffer 810 811 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 812 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 813 continue 814 } 815 if got := b.String(); test.want != got { 816 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 817 } 818 } 819 820 } 821 822 func TestErrors(t *testing.T) { 823 tests := []struct { 824 input string 825 err string 826 }{ 827 // Non-error cases. 828 { 829 "{{if .Cond}}<a>{{else}}<b>{{end}}", 830 "", 831 }, 832 { 833 "{{if .Cond}}<a>{{end}}", 834 "", 835 }, 836 { 837 "{{if .Cond}}{{else}}<b>{{end}}", 838 "", 839 }, 840 { 841 "{{with .Cond}}<div>{{end}}", 842 "", 843 }, 844 { 845 "{{range .Items}}<a>{{end}}", 846 "", 847 }, 848 { 849 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 850 "", 851 }, 852 // Error cases. 853 { 854 "{{if .Cond}}<a{{end}}", 855 "z:1: {{if}} branches", 856 }, 857 { 858 "{{if .Cond}}\n{{else}}\n<a{{end}}", 859 "z:1: {{if}} branches", 860 }, 861 { 862 // Missing quote in the else branch. 863 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 864 "z:1: {{if}} branches", 865 }, 866 { 867 // Different kind of attribute: href implies a URL. 868 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 869 "z:1: {{if}} branches", 870 }, 871 { 872 "\n{{with .X}}<a{{end}}", 873 "z:2: {{with}} branches", 874 }, 875 { 876 "\n{{with .X}}<a>{{else}}<a{{end}}", 877 "z:2: {{with}} branches", 878 }, 879 { 880 "{{range .Items}}<a{{end}}", 881 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 882 }, 883 { 884 "\n{{range .Items}} x='<a{{end}}", 885 "z:2: on range loop re-entry: {{range}} branches", 886 }, 887 { 888 "<a b=1 c={{.H}}", 889 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 890 }, 891 { 892 "<script>foo();", 893 "z: ends in a non-text context: {stateJS", 894 }, 895 { 896 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 897 "z:1: {{.H}} appears in an ambiguous URL context", 898 }, 899 { 900 `<a onclick="alert('Hello \`, 901 `unfinished escape sequence in JS string: "Hello \\"`, 902 }, 903 { 904 `<a onclick='alert("Hello\, World\`, 905 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 906 }, 907 { 908 `<a onclick='alert(/x+\`, 909 `unfinished escape sequence in JS string: "x+\\"`, 910 }, 911 { 912 `<a onclick="/foo[\]/`, 913 `unfinished JS regexp charset: "foo[\\]/"`, 914 }, 915 { 916 // It is ambiguous whether 1.5 should be 1\.5 or 1.5. 917 // Either `var x = 1/- 1.5 /i.test(x)` 918 // where `i.test(x)` is a method call of reference i, 919 // or `/-1\.5/i.test(x)` which is a method call on a 920 // case insensitive regular expression. 921 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, 922 `'/' could start a division or regexp: "/-"`, 923 }, 924 { 925 `{{template "foo"}}`, 926 "z:1: no such template \"foo\"", 927 }, 928 { 929 `<div{{template "y"}}>` + 930 // Illegal starting in stateTag but not in stateText. 931 `{{define "y"}} foo<b{{end}}`, 932 `"<" in attribute name: " foo<b"`, 933 }, 934 { 935 `<script>reverseList = [{{template "t"}}]</script>` + 936 // Missing " after recursive call. 937 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, 938 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, 939 }, 940 { 941 `<input type=button value=onclick=>`, 942 `html/template:z: "=" in unquoted attr: "onclick="`, 943 }, 944 { 945 `<input type=button value= onclick=>`, 946 `html/template:z: "=" in unquoted attr: "onclick="`, 947 }, 948 { 949 `<input type=button value= 1+1=2>`, 950 `html/template:z: "=" in unquoted attr: "1+1=2"`, 951 }, 952 { 953 "<a class=`foo>", 954 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 955 }, 956 { 957 `<a style=font:'Arial'>`, 958 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 959 }, 960 { 961 `<a=foo>`, 962 `: expected space, attr name, or end of tag, but got "=foo>"`, 963 }, 964 } 965 966 for _, test := range tests { 967 buf := new(bytes.Buffer) 968 tmpl, err := New("z").Parse(test.input) 969 if err != nil { 970 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 971 continue 972 } 973 err = tmpl.Execute(buf, nil) 974 var got string 975 if err != nil { 976 got = err.Error() 977 } 978 if test.err == "" { 979 if got != "" { 980 t.Errorf("input=%q: unexpected error %q", test.input, got) 981 } 982 continue 983 } 984 if strings.Index(got, test.err) == -1 { 985 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) 986 continue 987 } 988 } 989 } 990 991 func TestEscapeText(t *testing.T) { 992 tests := []struct { 993 input string 994 output context 995 }{ 996 { 997 ``, 998 context{}, 999 }, 1000 { 1001 `Hello, World!`, 1002 context{}, 1003 }, 1004 { 1005 // An orphaned "<" is OK. 1006 `I <3 Ponies!`, 1007 context{}, 1008 }, 1009 { 1010 `<a`, 1011 context{state: stateTag}, 1012 }, 1013 { 1014 `<a `, 1015 context{state: stateTag}, 1016 }, 1017 { 1018 `<a>`, 1019 context{state: stateText}, 1020 }, 1021 { 1022 `<a href`, 1023 context{state: stateAttrName, attr: attrURL}, 1024 }, 1025 { 1026 `<a on`, 1027 context{state: stateAttrName, attr: attrScript}, 1028 }, 1029 { 1030 `<a href `, 1031 context{state: stateAfterName, attr: attrURL}, 1032 }, 1033 { 1034 `<a style = `, 1035 context{state: stateBeforeValue, attr: attrStyle}, 1036 }, 1037 { 1038 `<a href=`, 1039 context{state: stateBeforeValue, attr: attrURL}, 1040 }, 1041 { 1042 `<a href=x`, 1043 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1044 }, 1045 { 1046 `<a href=x `, 1047 context{state: stateTag}, 1048 }, 1049 { 1050 `<a href=>`, 1051 context{state: stateText}, 1052 }, 1053 { 1054 `<a href=x>`, 1055 context{state: stateText}, 1056 }, 1057 { 1058 `<a href ='`, 1059 context{state: stateURL, delim: delimSingleQuote}, 1060 }, 1061 { 1062 `<a href=''`, 1063 context{state: stateTag}, 1064 }, 1065 { 1066 `<a href= "`, 1067 context{state: stateURL, delim: delimDoubleQuote}, 1068 }, 1069 { 1070 `<a href=""`, 1071 context{state: stateTag}, 1072 }, 1073 { 1074 `<a title="`, 1075 context{state: stateAttr, delim: delimDoubleQuote}, 1076 }, 1077 { 1078 `<a HREF='http:`, 1079 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1080 }, 1081 { 1082 `<a Href='/`, 1083 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1084 }, 1085 { 1086 `<a href='"`, 1087 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1088 }, 1089 { 1090 `<a href="'`, 1091 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1092 }, 1093 { 1094 `<a href=''`, 1095 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1096 }, 1097 { 1098 `<a href=""`, 1099 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1100 }, 1101 { 1102 `<a href=""`, 1103 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1104 }, 1105 { 1106 `<a href="`, 1107 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1108 }, 1109 { 1110 `<img alt="1">`, 1111 context{state: stateText}, 1112 }, 1113 { 1114 `<img alt="1>"`, 1115 context{state: stateTag}, 1116 }, 1117 { 1118 `<img alt="1>">`, 1119 context{state: stateText}, 1120 }, 1121 { 1122 `<input checked type="checkbox"`, 1123 context{state: stateTag}, 1124 }, 1125 { 1126 `<a onclick="`, 1127 context{state: stateJS, delim: delimDoubleQuote}, 1128 }, 1129 { 1130 `<a onclick="//foo`, 1131 context{state: stateJSLineCmt, delim: delimDoubleQuote}, 1132 }, 1133 { 1134 "<a onclick='//\n", 1135 context{state: stateJS, delim: delimSingleQuote}, 1136 }, 1137 { 1138 "<a onclick='//\r\n", 1139 context{state: stateJS, delim: delimSingleQuote}, 1140 }, 1141 { 1142 "<a onclick='//\u2028", 1143 context{state: stateJS, delim: delimSingleQuote}, 1144 }, 1145 { 1146 `<a onclick="/*`, 1147 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1148 }, 1149 { 1150 `<a onclick="/*/`, 1151 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1152 }, 1153 { 1154 `<a onclick="/**/`, 1155 context{state: stateJS, delim: delimDoubleQuote}, 1156 }, 1157 { 1158 `<a onkeypress=""`, 1159 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1160 }, 1161 { 1162 `<a onclick='"foo"`, 1163 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp}, 1164 }, 1165 { 1166 `<a onclick='foo'`, 1167 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp}, 1168 }, 1169 { 1170 `<a onclick='foo`, 1171 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd}, 1172 }, 1173 { 1174 `<a onclick=""foo'`, 1175 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1176 }, 1177 { 1178 `<a onclick="'foo"`, 1179 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1180 }, 1181 { 1182 `<A ONCLICK="'`, 1183 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1184 }, 1185 { 1186 `<a onclick="/`, 1187 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1188 }, 1189 { 1190 `<a onclick="'foo'`, 1191 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1192 }, 1193 { 1194 `<a onclick="'foo\'`, 1195 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1196 }, 1197 { 1198 `<a onclick="'foo\'`, 1199 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1200 }, 1201 { 1202 `<a onclick="/foo/`, 1203 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1204 }, 1205 { 1206 `<script>/foo/ /=`, 1207 context{state: stateJS, element: elementScript}, 1208 }, 1209 { 1210 `<a onclick="1 /foo`, 1211 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1212 }, 1213 { 1214 `<a onclick="1 /*c*/ /foo`, 1215 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1216 }, 1217 { 1218 `<a onclick="/foo[/]`, 1219 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1220 }, 1221 { 1222 `<a onclick="/foo\/`, 1223 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1224 }, 1225 { 1226 `<a onclick="/foo/`, 1227 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1228 }, 1229 { 1230 `<input checked style="`, 1231 context{state: stateCSS, delim: delimDoubleQuote}, 1232 }, 1233 { 1234 `<a style="//`, 1235 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1236 }, 1237 { 1238 `<a style="//</script>`, 1239 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1240 }, 1241 { 1242 "<a style='//\n", 1243 context{state: stateCSS, delim: delimSingleQuote}, 1244 }, 1245 { 1246 "<a style='//\r", 1247 context{state: stateCSS, delim: delimSingleQuote}, 1248 }, 1249 { 1250 `<a style="/*`, 1251 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1252 }, 1253 { 1254 `<a style="/*/`, 1255 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1256 }, 1257 { 1258 `<a style="/**/`, 1259 context{state: stateCSS, delim: delimDoubleQuote}, 1260 }, 1261 { 1262 `<a style="background: '`, 1263 context{state: stateCSSSqStr, delim: delimDoubleQuote}, 1264 }, 1265 { 1266 `<a style="background: "`, 1267 context{state: stateCSSDqStr, delim: delimDoubleQuote}, 1268 }, 1269 { 1270 `<a style="background: '/foo?img=`, 1271 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1272 }, 1273 { 1274 `<a style="background: '/`, 1275 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1276 }, 1277 { 1278 `<a style="background: url("/`, 1279 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1280 }, 1281 { 1282 `<a style="background: url('/`, 1283 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1284 }, 1285 { 1286 `<a style="background: url('/)`, 1287 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1288 }, 1289 { 1290 `<a style="background: url('/ `, 1291 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1292 }, 1293 { 1294 `<a style="background: url(/`, 1295 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1296 }, 1297 { 1298 `<a style="background: url( `, 1299 context{state: stateCSSURL, delim: delimDoubleQuote}, 1300 }, 1301 { 1302 `<a style="background: url( /image?name=`, 1303 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1304 }, 1305 { 1306 `<a style="background: url(x)`, 1307 context{state: stateCSS, delim: delimDoubleQuote}, 1308 }, 1309 { 1310 `<a style="background: url('x'`, 1311 context{state: stateCSS, delim: delimDoubleQuote}, 1312 }, 1313 { 1314 `<a style="background: url( x `, 1315 context{state: stateCSS, delim: delimDoubleQuote}, 1316 }, 1317 { 1318 `<!-- foo`, 1319 context{state: stateHTMLCmt}, 1320 }, 1321 { 1322 `<!-->`, 1323 context{state: stateHTMLCmt}, 1324 }, 1325 { 1326 `<!--->`, 1327 context{state: stateHTMLCmt}, 1328 }, 1329 { 1330 `<!-- foo -->`, 1331 context{state: stateText}, 1332 }, 1333 { 1334 `<script`, 1335 context{state: stateTag, element: elementScript}, 1336 }, 1337 { 1338 `<script `, 1339 context{state: stateTag, element: elementScript}, 1340 }, 1341 { 1342 `<script src="foo.js" `, 1343 context{state: stateTag, element: elementScript}, 1344 }, 1345 { 1346 `<script src='foo.js' `, 1347 context{state: stateTag, element: elementScript}, 1348 }, 1349 { 1350 `<script type=text/javascript `, 1351 context{state: stateTag, element: elementScript}, 1352 }, 1353 { 1354 `<script>foo`, 1355 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1356 }, 1357 { 1358 `<script>foo</script>`, 1359 context{state: stateText}, 1360 }, 1361 { 1362 `<script>foo</script><!--`, 1363 context{state: stateHTMLCmt}, 1364 }, 1365 { 1366 `<script>document.write("<p>foo</p>");`, 1367 context{state: stateJS, element: elementScript}, 1368 }, 1369 { 1370 `<script>document.write("<p>foo<\/script>");`, 1371 context{state: stateJS, element: elementScript}, 1372 }, 1373 { 1374 `<script>document.write("<script>alert(1)</script>");`, 1375 context{state: stateText}, 1376 }, 1377 { 1378 `<Script>`, 1379 context{state: stateJS, element: elementScript}, 1380 }, 1381 { 1382 `<SCRIPT>foo`, 1383 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1384 }, 1385 { 1386 `<textarea>value`, 1387 context{state: stateRCDATA, element: elementTextarea}, 1388 }, 1389 { 1390 `<textarea>value</TEXTAREA>`, 1391 context{state: stateText}, 1392 }, 1393 { 1394 `<textarea name=html><b`, 1395 context{state: stateRCDATA, element: elementTextarea}, 1396 }, 1397 { 1398 `<title>value`, 1399 context{state: stateRCDATA, element: elementTitle}, 1400 }, 1401 { 1402 `<style>value`, 1403 context{state: stateCSS, element: elementStyle}, 1404 }, 1405 { 1406 `<a xlink:href`, 1407 context{state: stateAttrName, attr: attrURL}, 1408 }, 1409 { 1410 `<a xmlns`, 1411 context{state: stateAttrName, attr: attrURL}, 1412 }, 1413 { 1414 `<a xmlns:foo`, 1415 context{state: stateAttrName, attr: attrURL}, 1416 }, 1417 { 1418 `<a xmlnsxyz`, 1419 context{state: stateAttrName}, 1420 }, 1421 { 1422 `<a data-url`, 1423 context{state: stateAttrName, attr: attrURL}, 1424 }, 1425 { 1426 `<a data-iconUri`, 1427 context{state: stateAttrName, attr: attrURL}, 1428 }, 1429 { 1430 `<a data-urlItem`, 1431 context{state: stateAttrName, attr: attrURL}, 1432 }, 1433 { 1434 `<a g:`, 1435 context{state: stateAttrName}, 1436 }, 1437 { 1438 `<a g:url`, 1439 context{state: stateAttrName, attr: attrURL}, 1440 }, 1441 { 1442 `<a g:iconUri`, 1443 context{state: stateAttrName, attr: attrURL}, 1444 }, 1445 { 1446 `<a g:urlItem`, 1447 context{state: stateAttrName, attr: attrURL}, 1448 }, 1449 { 1450 `<a g:value`, 1451 context{state: stateAttrName}, 1452 }, 1453 { 1454 `<a svg:style='`, 1455 context{state: stateCSS, delim: delimSingleQuote}, 1456 }, 1457 { 1458 `<svg:font-face`, 1459 context{state: stateTag}, 1460 }, 1461 { 1462 `<svg:a svg:onclick="`, 1463 context{state: stateJS, delim: delimDoubleQuote}, 1464 }, 1465 } 1466 1467 for _, test := range tests { 1468 b, e := []byte(test.input), newEscaper(nil) 1469 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1470 if !test.output.eq(c) { 1471 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1472 continue 1473 } 1474 if test.input != string(b) { 1475 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1476 continue 1477 } 1478 } 1479 } 1480 1481 func TestEnsurePipelineContains(t *testing.T) { 1482 tests := []struct { 1483 input, output string 1484 ids []string 1485 }{ 1486 { 1487 "{{.X}}", 1488 ".X", 1489 []string{}, 1490 }, 1491 { 1492 "{{.X | html}}", 1493 ".X | html", 1494 []string{}, 1495 }, 1496 { 1497 "{{.X}}", 1498 ".X | html", 1499 []string{"html"}, 1500 }, 1501 { 1502 "{{.X | html}}", 1503 ".X | html | urlquery", 1504 []string{"urlquery"}, 1505 }, 1506 { 1507 "{{.X | html | urlquery}}", 1508 ".X | html | urlquery", 1509 []string{"urlquery"}, 1510 }, 1511 { 1512 "{{.X | html | urlquery}}", 1513 ".X | html | urlquery", 1514 []string{"html", "urlquery"}, 1515 }, 1516 { 1517 "{{.X | html | urlquery}}", 1518 ".X | html | urlquery", 1519 []string{"html"}, 1520 }, 1521 { 1522 "{{.X | urlquery}}", 1523 ".X | html | urlquery", 1524 []string{"html", "urlquery"}, 1525 }, 1526 { 1527 "{{.X | html | print}}", 1528 ".X | urlquery | html | print", 1529 []string{"urlquery", "html"}, 1530 }, 1531 { 1532 "{{($).X | html | print}}", 1533 "($).X | urlquery | html | print", 1534 []string{"urlquery", "html"}, 1535 }, 1536 } 1537 for i, test := range tests { 1538 tmpl := template.Must(template.New("test").Parse(test.input)) 1539 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1540 if !ok { 1541 t.Errorf("#%d: First node is not an action: %s", i, test.input) 1542 continue 1543 } 1544 pipe := action.Pipe 1545 ensurePipelineContains(pipe, test.ids) 1546 got := pipe.String() 1547 if got != test.output { 1548 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got) 1549 } 1550 } 1551 } 1552 1553 func TestEscapeErrorsNotIgnorable(t *testing.T) { 1554 var b bytes.Buffer 1555 tmpl, _ := New("dangerous").Parse("<a") 1556 err := tmpl.Execute(&b, nil) 1557 if err == nil { 1558 t.Errorf("Expected error") 1559 } else if b.Len() != 0 { 1560 t.Errorf("Emitted output despite escaping failure") 1561 } 1562 } 1563 1564 func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1565 var b bytes.Buffer 1566 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1567 if err != nil { 1568 t.Errorf("failed to parse set: %q", err) 1569 } 1570 err = tmpl.ExecuteTemplate(&b, "t", nil) 1571 if err == nil { 1572 t.Errorf("Expected error") 1573 } else if b.Len() != 0 { 1574 t.Errorf("Emitted output despite escaping failure") 1575 } 1576 } 1577 1578 func TestRedundantFuncs(t *testing.T) { 1579 inputs := []interface{}{ 1580 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1581 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1582 ` !"#$%&'()*+,-./` + 1583 `0123456789:;<=>?` + 1584 `@ABCDEFGHIJKLMNO` + 1585 `PQRSTUVWXYZ[\]^_` + 1586 "`abcdefghijklmno" + 1587 "pqrstuvwxyz{|}~\x7f" + 1588 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 1589 "&%22\\", 1590 CSS(`a[href =~ "//example.com"]#foo`), 1591 HTML(`Hello, <b>World</b> &tc!`), 1592 HTMLAttr(` dir="ltr"`), 1593 JS(`c && alert("Hello, World!");`), 1594 JSStr(`Hello, World & O'Reilly\x21`), 1595 URL(`greeting=H%69&addressee=(World)`), 1596 } 1597 1598 for n0, m := range redundantFuncs { 1599 f0 := funcMap[n0].(func(...interface{}) string) 1600 for n1 := range m { 1601 f1 := funcMap[n1].(func(...interface{}) string) 1602 for _, input := range inputs { 1603 want := f0(input) 1604 if got := f1(want); want != got { 1605 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 1606 } 1607 } 1608 } 1609 } 1610 } 1611 1612 func TestIndirectPrint(t *testing.T) { 1613 a := 3 1614 ap := &a 1615 b := "hello" 1616 bp := &b 1617 bpp := &bp 1618 tmpl := Must(New("t").Parse(`{{.}}`)) 1619 var buf bytes.Buffer 1620 err := tmpl.Execute(&buf, ap) 1621 if err != nil { 1622 t.Errorf("Unexpected error: %s", err) 1623 } else if buf.String() != "3" { 1624 t.Errorf(`Expected "3"; got %q`, buf.String()) 1625 } 1626 buf.Reset() 1627 err = tmpl.Execute(&buf, bpp) 1628 if err != nil { 1629 t.Errorf("Unexpected error: %s", err) 1630 } else if buf.String() != "hello" { 1631 t.Errorf(`Expected "hello"; got %q`, buf.String()) 1632 } 1633 } 1634 1635 // This is a test for issue 3272. 1636 func TestEmptyTemplate(t *testing.T) { 1637 page := Must(New("page").ParseFiles(os.DevNull)) 1638 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 1639 t.Fatal("expected error") 1640 } 1641 } 1642 1643 func BenchmarkEscapedExecute(b *testing.B) { 1644 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 1645 var buf bytes.Buffer 1646 b.ResetTimer() 1647 for i := 0; i < b.N; i++ { 1648 tmpl.Execute(&buf, "foo & 'bar' & baz") 1649 buf.Reset() 1650 } 1651 }