github.com/ader1990/go@v0.0.0-20140630135419-8c24447fa791/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 // Check for bug 6459: Tree field was not set in Parse. 659 if tmpl.Tree != tmpl.text.Tree { 660 t.Errorf("%s: tree not set properly", test.name) 661 continue 662 } 663 b := new(bytes.Buffer) 664 if err := tmpl.Execute(b, data); err != nil { 665 t.Errorf("%s: template execution failed: %s", test.name, err) 666 continue 667 } 668 if w, g := test.output, b.String(); w != g { 669 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) 670 continue 671 } 672 b.Reset() 673 if err := tmpl.Execute(b, pdata); err != nil { 674 t.Errorf("%s: template execution failed for pointer: %s", test.name, err) 675 continue 676 } 677 if w, g := test.output, b.String(); w != g { 678 t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) 679 continue 680 } 681 if tmpl.Tree != tmpl.text.Tree { 682 t.Errorf("%s: tree mismatch", test.name) 683 continue 684 } 685 } 686 } 687 688 func TestEscapeSet(t *testing.T) { 689 type dataItem struct { 690 Children []*dataItem 691 X string 692 } 693 694 data := dataItem{ 695 Children: []*dataItem{ 696 {X: "foo"}, 697 {X: "<bar>"}, 698 { 699 Children: []*dataItem{ 700 {X: "baz"}, 701 }, 702 }, 703 }, 704 } 705 706 tests := []struct { 707 inputs map[string]string 708 want string 709 }{ 710 // The trivial set. 711 { 712 map[string]string{ 713 "main": ``, 714 }, 715 ``, 716 }, 717 // A template called in the start context. 718 { 719 map[string]string{ 720 "main": `Hello, {{template "helper"}}!`, 721 // Not a valid top level HTML template. 722 // "<b" is not a full tag. 723 "helper": `{{"<World>"}}`, 724 }, 725 `Hello, <World>!`, 726 }, 727 // A template called in a context other than the start. 728 { 729 map[string]string{ 730 "main": `<a onclick='a = {{template "helper"}};'>`, 731 // Not a valid top level HTML template. 732 // "<b" is not a full tag. 733 "helper": `{{"<a>"}}<b`, 734 }, 735 `<a onclick='a = "\u003ca\u003e"<b;'>`, 736 }, 737 // A recursive template that ends in its start context. 738 { 739 map[string]string{ 740 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 741 }, 742 `foo <bar> baz `, 743 }, 744 // A recursive helper template that ends in its start context. 745 { 746 map[string]string{ 747 "main": `{{template "helper" .}}`, 748 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 749 }, 750 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 751 }, 752 // Co-recursive templates that end in its start context. 753 { 754 map[string]string{ 755 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 756 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 757 }, 758 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 759 }, 760 // A template that is called in two different contexts. 761 { 762 map[string]string{ 763 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 764 "helper": `{{11}} of {{"<100>"}}`, 765 }, 766 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`, 767 }, 768 // A non-recursive template that ends in a different context. 769 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 770 { 771 map[string]string{ 772 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 773 "helper": "{{126}}", 774 }, 775 `<script>var x= 126 /"42";</script>`, 776 }, 777 // A recursive template that ends in a similar context. 778 { 779 map[string]string{ 780 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 781 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 782 }, 783 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 784 }, 785 // A recursive template that ends in a different context. 786 /* 787 { 788 map[string]string{ 789 "main": `<a href="/foo{{template "helper" .}}">`, 790 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 791 }, 792 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 793 }, 794 */ 795 } 796 797 // pred is a template function that returns the predecessor of a 798 // natural number for testing recursive templates. 799 fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) { 800 if len(a) == 1 { 801 if i, _ := a[0].(int); i > 0 { 802 return i - 1, nil 803 } 804 } 805 return nil, fmt.Errorf("undefined pred(%v)", a) 806 }} 807 808 for _, test := range tests { 809 source := "" 810 for name, body := range test.inputs { 811 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 812 } 813 tmpl, err := New("root").Funcs(fns).Parse(source) 814 if err != nil { 815 t.Errorf("error parsing %q: %v", source, err) 816 continue 817 } 818 var b bytes.Buffer 819 820 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 821 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 822 continue 823 } 824 if got := b.String(); test.want != got { 825 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 826 } 827 } 828 829 } 830 831 func TestErrors(t *testing.T) { 832 tests := []struct { 833 input string 834 err string 835 }{ 836 // Non-error cases. 837 { 838 "{{if .Cond}}<a>{{else}}<b>{{end}}", 839 "", 840 }, 841 { 842 "{{if .Cond}}<a>{{end}}", 843 "", 844 }, 845 { 846 "{{if .Cond}}{{else}}<b>{{end}}", 847 "", 848 }, 849 { 850 "{{with .Cond}}<div>{{end}}", 851 "", 852 }, 853 { 854 "{{range .Items}}<a>{{end}}", 855 "", 856 }, 857 { 858 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 859 "", 860 }, 861 // Error cases. 862 { 863 "{{if .Cond}}<a{{end}}", 864 "z:1: {{if}} branches", 865 }, 866 { 867 "{{if .Cond}}\n{{else}}\n<a{{end}}", 868 "z:1: {{if}} branches", 869 }, 870 { 871 // Missing quote in the else branch. 872 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 873 "z:1: {{if}} branches", 874 }, 875 { 876 // Different kind of attribute: href implies a URL. 877 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 878 "z:1: {{if}} branches", 879 }, 880 { 881 "\n{{with .X}}<a{{end}}", 882 "z:2: {{with}} branches", 883 }, 884 { 885 "\n{{with .X}}<a>{{else}}<a{{end}}", 886 "z:2: {{with}} branches", 887 }, 888 { 889 "{{range .Items}}<a{{end}}", 890 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 891 }, 892 { 893 "\n{{range .Items}} x='<a{{end}}", 894 "z:2: on range loop re-entry: {{range}} branches", 895 }, 896 { 897 "<a b=1 c={{.H}}", 898 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 899 }, 900 { 901 "<script>foo();", 902 "z: ends in a non-text context: {stateJS", 903 }, 904 { 905 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 906 "z:1: {{.H}} appears in an ambiguous URL context", 907 }, 908 { 909 `<a onclick="alert('Hello \`, 910 `unfinished escape sequence in JS string: "Hello \\"`, 911 }, 912 { 913 `<a onclick='alert("Hello\, World\`, 914 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 915 }, 916 { 917 `<a onclick='alert(/x+\`, 918 `unfinished escape sequence in JS string: "x+\\"`, 919 }, 920 { 921 `<a onclick="/foo[\]/`, 922 `unfinished JS regexp charset: "foo[\\]/"`, 923 }, 924 { 925 // It is ambiguous whether 1.5 should be 1\.5 or 1.5. 926 // Either `var x = 1/- 1.5 /i.test(x)` 927 // where `i.test(x)` is a method call of reference i, 928 // or `/-1\.5/i.test(x)` which is a method call on a 929 // case insensitive regular expression. 930 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, 931 `'/' could start a division or regexp: "/-"`, 932 }, 933 { 934 `{{template "foo"}}`, 935 "z:1: no such template \"foo\"", 936 }, 937 { 938 `<div{{template "y"}}>` + 939 // Illegal starting in stateTag but not in stateText. 940 `{{define "y"}} foo<b{{end}}`, 941 `"<" in attribute name: " foo<b"`, 942 }, 943 { 944 `<script>reverseList = [{{template "t"}}]</script>` + 945 // Missing " after recursive call. 946 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, 947 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, 948 }, 949 { 950 `<input type=button value=onclick=>`, 951 `html/template:z: "=" in unquoted attr: "onclick="`, 952 }, 953 { 954 `<input type=button value= onclick=>`, 955 `html/template:z: "=" in unquoted attr: "onclick="`, 956 }, 957 { 958 `<input type=button value= 1+1=2>`, 959 `html/template:z: "=" in unquoted attr: "1+1=2"`, 960 }, 961 { 962 "<a class=`foo>", 963 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 964 }, 965 { 966 `<a style=font:'Arial'>`, 967 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 968 }, 969 { 970 `<a=foo>`, 971 `: expected space, attr name, or end of tag, but got "=foo>"`, 972 }, 973 } 974 975 for _, test := range tests { 976 buf := new(bytes.Buffer) 977 tmpl, err := New("z").Parse(test.input) 978 if err != nil { 979 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 980 continue 981 } 982 err = tmpl.Execute(buf, nil) 983 var got string 984 if err != nil { 985 got = err.Error() 986 } 987 if test.err == "" { 988 if got != "" { 989 t.Errorf("input=%q: unexpected error %q", test.input, got) 990 } 991 continue 992 } 993 if strings.Index(got, test.err) == -1 { 994 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) 995 continue 996 } 997 } 998 } 999 1000 func TestEscapeText(t *testing.T) { 1001 tests := []struct { 1002 input string 1003 output context 1004 }{ 1005 { 1006 ``, 1007 context{}, 1008 }, 1009 { 1010 `Hello, World!`, 1011 context{}, 1012 }, 1013 { 1014 // An orphaned "<" is OK. 1015 `I <3 Ponies!`, 1016 context{}, 1017 }, 1018 { 1019 `<a`, 1020 context{state: stateTag}, 1021 }, 1022 { 1023 `<a `, 1024 context{state: stateTag}, 1025 }, 1026 { 1027 `<a>`, 1028 context{state: stateText}, 1029 }, 1030 { 1031 `<a href`, 1032 context{state: stateAttrName, attr: attrURL}, 1033 }, 1034 { 1035 `<a on`, 1036 context{state: stateAttrName, attr: attrScript}, 1037 }, 1038 { 1039 `<a href `, 1040 context{state: stateAfterName, attr: attrURL}, 1041 }, 1042 { 1043 `<a style = `, 1044 context{state: stateBeforeValue, attr: attrStyle}, 1045 }, 1046 { 1047 `<a href=`, 1048 context{state: stateBeforeValue, attr: attrURL}, 1049 }, 1050 { 1051 `<a href=x`, 1052 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1053 }, 1054 { 1055 `<a href=x `, 1056 context{state: stateTag}, 1057 }, 1058 { 1059 `<a href=>`, 1060 context{state: stateText}, 1061 }, 1062 { 1063 `<a href=x>`, 1064 context{state: stateText}, 1065 }, 1066 { 1067 `<a href ='`, 1068 context{state: stateURL, delim: delimSingleQuote}, 1069 }, 1070 { 1071 `<a href=''`, 1072 context{state: stateTag}, 1073 }, 1074 { 1075 `<a href= "`, 1076 context{state: stateURL, delim: delimDoubleQuote}, 1077 }, 1078 { 1079 `<a href=""`, 1080 context{state: stateTag}, 1081 }, 1082 { 1083 `<a title="`, 1084 context{state: stateAttr, delim: delimDoubleQuote}, 1085 }, 1086 { 1087 `<a HREF='http:`, 1088 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1089 }, 1090 { 1091 `<a Href='/`, 1092 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1093 }, 1094 { 1095 `<a href='"`, 1096 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1097 }, 1098 { 1099 `<a href="'`, 1100 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1101 }, 1102 { 1103 `<a href=''`, 1104 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1105 }, 1106 { 1107 `<a href=""`, 1108 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1109 }, 1110 { 1111 `<a href=""`, 1112 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1113 }, 1114 { 1115 `<a href="`, 1116 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1117 }, 1118 { 1119 `<img alt="1">`, 1120 context{state: stateText}, 1121 }, 1122 { 1123 `<img alt="1>"`, 1124 context{state: stateTag}, 1125 }, 1126 { 1127 `<img alt="1>">`, 1128 context{state: stateText}, 1129 }, 1130 { 1131 `<input checked type="checkbox"`, 1132 context{state: stateTag}, 1133 }, 1134 { 1135 `<a onclick="`, 1136 context{state: stateJS, delim: delimDoubleQuote}, 1137 }, 1138 { 1139 `<a onclick="//foo`, 1140 context{state: stateJSLineCmt, delim: delimDoubleQuote}, 1141 }, 1142 { 1143 "<a onclick='//\n", 1144 context{state: stateJS, delim: delimSingleQuote}, 1145 }, 1146 { 1147 "<a onclick='//\r\n", 1148 context{state: stateJS, delim: delimSingleQuote}, 1149 }, 1150 { 1151 "<a onclick='//\u2028", 1152 context{state: stateJS, delim: delimSingleQuote}, 1153 }, 1154 { 1155 `<a onclick="/*`, 1156 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1157 }, 1158 { 1159 `<a onclick="/*/`, 1160 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1161 }, 1162 { 1163 `<a onclick="/**/`, 1164 context{state: stateJS, delim: delimDoubleQuote}, 1165 }, 1166 { 1167 `<a onkeypress=""`, 1168 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1169 }, 1170 { 1171 `<a onclick='"foo"`, 1172 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp}, 1173 }, 1174 { 1175 `<a onclick='foo'`, 1176 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp}, 1177 }, 1178 { 1179 `<a onclick='foo`, 1180 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd}, 1181 }, 1182 { 1183 `<a onclick=""foo'`, 1184 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1185 }, 1186 { 1187 `<a onclick="'foo"`, 1188 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1189 }, 1190 { 1191 `<A ONCLICK="'`, 1192 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1193 }, 1194 { 1195 `<a onclick="/`, 1196 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1197 }, 1198 { 1199 `<a onclick="'foo'`, 1200 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1201 }, 1202 { 1203 `<a onclick="'foo\'`, 1204 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1205 }, 1206 { 1207 `<a onclick="'foo\'`, 1208 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1209 }, 1210 { 1211 `<a onclick="/foo/`, 1212 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1213 }, 1214 { 1215 `<script>/foo/ /=`, 1216 context{state: stateJS, element: elementScript}, 1217 }, 1218 { 1219 `<a onclick="1 /foo`, 1220 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1221 }, 1222 { 1223 `<a onclick="1 /*c*/ /foo`, 1224 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1225 }, 1226 { 1227 `<a onclick="/foo[/]`, 1228 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1229 }, 1230 { 1231 `<a onclick="/foo\/`, 1232 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1233 }, 1234 { 1235 `<a onclick="/foo/`, 1236 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1237 }, 1238 { 1239 `<input checked style="`, 1240 context{state: stateCSS, delim: delimDoubleQuote}, 1241 }, 1242 { 1243 `<a style="//`, 1244 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1245 }, 1246 { 1247 `<a style="//</script>`, 1248 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1249 }, 1250 { 1251 "<a style='//\n", 1252 context{state: stateCSS, delim: delimSingleQuote}, 1253 }, 1254 { 1255 "<a style='//\r", 1256 context{state: stateCSS, delim: delimSingleQuote}, 1257 }, 1258 { 1259 `<a style="/*`, 1260 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1261 }, 1262 { 1263 `<a style="/*/`, 1264 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1265 }, 1266 { 1267 `<a style="/**/`, 1268 context{state: stateCSS, delim: delimDoubleQuote}, 1269 }, 1270 { 1271 `<a style="background: '`, 1272 context{state: stateCSSSqStr, delim: delimDoubleQuote}, 1273 }, 1274 { 1275 `<a style="background: "`, 1276 context{state: stateCSSDqStr, delim: delimDoubleQuote}, 1277 }, 1278 { 1279 `<a style="background: '/foo?img=`, 1280 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1281 }, 1282 { 1283 `<a style="background: '/`, 1284 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1285 }, 1286 { 1287 `<a style="background: url("/`, 1288 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1289 }, 1290 { 1291 `<a style="background: url('/`, 1292 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1293 }, 1294 { 1295 `<a style="background: url('/)`, 1296 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1297 }, 1298 { 1299 `<a style="background: url('/ `, 1300 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1301 }, 1302 { 1303 `<a style="background: url(/`, 1304 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1305 }, 1306 { 1307 `<a style="background: url( `, 1308 context{state: stateCSSURL, delim: delimDoubleQuote}, 1309 }, 1310 { 1311 `<a style="background: url( /image?name=`, 1312 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1313 }, 1314 { 1315 `<a style="background: url(x)`, 1316 context{state: stateCSS, delim: delimDoubleQuote}, 1317 }, 1318 { 1319 `<a style="background: url('x'`, 1320 context{state: stateCSS, delim: delimDoubleQuote}, 1321 }, 1322 { 1323 `<a style="background: url( x `, 1324 context{state: stateCSS, delim: delimDoubleQuote}, 1325 }, 1326 { 1327 `<!-- foo`, 1328 context{state: stateHTMLCmt}, 1329 }, 1330 { 1331 `<!-->`, 1332 context{state: stateHTMLCmt}, 1333 }, 1334 { 1335 `<!--->`, 1336 context{state: stateHTMLCmt}, 1337 }, 1338 { 1339 `<!-- foo -->`, 1340 context{state: stateText}, 1341 }, 1342 { 1343 `<script`, 1344 context{state: stateTag, element: elementScript}, 1345 }, 1346 { 1347 `<script `, 1348 context{state: stateTag, element: elementScript}, 1349 }, 1350 { 1351 `<script src="foo.js" `, 1352 context{state: stateTag, element: elementScript}, 1353 }, 1354 { 1355 `<script src='foo.js' `, 1356 context{state: stateTag, element: elementScript}, 1357 }, 1358 { 1359 `<script type=text/javascript `, 1360 context{state: stateTag, element: elementScript}, 1361 }, 1362 { 1363 `<script>foo`, 1364 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1365 }, 1366 { 1367 `<script>foo</script>`, 1368 context{state: stateText}, 1369 }, 1370 { 1371 `<script>foo</script><!--`, 1372 context{state: stateHTMLCmt}, 1373 }, 1374 { 1375 `<script>document.write("<p>foo</p>");`, 1376 context{state: stateJS, element: elementScript}, 1377 }, 1378 { 1379 `<script>document.write("<p>foo<\/script>");`, 1380 context{state: stateJS, element: elementScript}, 1381 }, 1382 { 1383 `<script>document.write("<script>alert(1)</script>");`, 1384 context{state: stateText}, 1385 }, 1386 { 1387 `<Script>`, 1388 context{state: stateJS, element: elementScript}, 1389 }, 1390 { 1391 `<SCRIPT>foo`, 1392 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1393 }, 1394 { 1395 `<textarea>value`, 1396 context{state: stateRCDATA, element: elementTextarea}, 1397 }, 1398 { 1399 `<textarea>value</TEXTAREA>`, 1400 context{state: stateText}, 1401 }, 1402 { 1403 `<textarea name=html><b`, 1404 context{state: stateRCDATA, element: elementTextarea}, 1405 }, 1406 { 1407 `<title>value`, 1408 context{state: stateRCDATA, element: elementTitle}, 1409 }, 1410 { 1411 `<style>value`, 1412 context{state: stateCSS, element: elementStyle}, 1413 }, 1414 { 1415 `<a xlink:href`, 1416 context{state: stateAttrName, attr: attrURL}, 1417 }, 1418 { 1419 `<a xmlns`, 1420 context{state: stateAttrName, attr: attrURL}, 1421 }, 1422 { 1423 `<a xmlns:foo`, 1424 context{state: stateAttrName, attr: attrURL}, 1425 }, 1426 { 1427 `<a xmlnsxyz`, 1428 context{state: stateAttrName}, 1429 }, 1430 { 1431 `<a data-url`, 1432 context{state: stateAttrName, attr: attrURL}, 1433 }, 1434 { 1435 `<a data-iconUri`, 1436 context{state: stateAttrName, attr: attrURL}, 1437 }, 1438 { 1439 `<a data-urlItem`, 1440 context{state: stateAttrName, attr: attrURL}, 1441 }, 1442 { 1443 `<a g:`, 1444 context{state: stateAttrName}, 1445 }, 1446 { 1447 `<a g:url`, 1448 context{state: stateAttrName, attr: attrURL}, 1449 }, 1450 { 1451 `<a g:iconUri`, 1452 context{state: stateAttrName, attr: attrURL}, 1453 }, 1454 { 1455 `<a g:urlItem`, 1456 context{state: stateAttrName, attr: attrURL}, 1457 }, 1458 { 1459 `<a g:value`, 1460 context{state: stateAttrName}, 1461 }, 1462 { 1463 `<a svg:style='`, 1464 context{state: stateCSS, delim: delimSingleQuote}, 1465 }, 1466 { 1467 `<svg:font-face`, 1468 context{state: stateTag}, 1469 }, 1470 { 1471 `<svg:a svg:onclick="`, 1472 context{state: stateJS, delim: delimDoubleQuote}, 1473 }, 1474 } 1475 1476 for _, test := range tests { 1477 b, e := []byte(test.input), newEscaper(nil) 1478 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1479 if !test.output.eq(c) { 1480 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1481 continue 1482 } 1483 if test.input != string(b) { 1484 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1485 continue 1486 } 1487 } 1488 } 1489 1490 func TestEnsurePipelineContains(t *testing.T) { 1491 tests := []struct { 1492 input, output string 1493 ids []string 1494 }{ 1495 { 1496 "{{.X}}", 1497 ".X", 1498 []string{}, 1499 }, 1500 { 1501 "{{.X | html}}", 1502 ".X | html", 1503 []string{}, 1504 }, 1505 { 1506 "{{.X}}", 1507 ".X | html", 1508 []string{"html"}, 1509 }, 1510 { 1511 "{{.X | html}}", 1512 ".X | html | urlquery", 1513 []string{"urlquery"}, 1514 }, 1515 { 1516 "{{.X | html | urlquery}}", 1517 ".X | html | urlquery", 1518 []string{"urlquery"}, 1519 }, 1520 { 1521 "{{.X | html | urlquery}}", 1522 ".X | html | urlquery", 1523 []string{"html", "urlquery"}, 1524 }, 1525 { 1526 "{{.X | html | urlquery}}", 1527 ".X | html | urlquery", 1528 []string{"html"}, 1529 }, 1530 { 1531 "{{.X | urlquery}}", 1532 ".X | html | urlquery", 1533 []string{"html", "urlquery"}, 1534 }, 1535 { 1536 "{{.X | html | print}}", 1537 ".X | urlquery | html | print", 1538 []string{"urlquery", "html"}, 1539 }, 1540 { 1541 "{{($).X | html | print}}", 1542 "($).X | urlquery | html | print", 1543 []string{"urlquery", "html"}, 1544 }, 1545 } 1546 for i, test := range tests { 1547 tmpl := template.Must(template.New("test").Parse(test.input)) 1548 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1549 if !ok { 1550 t.Errorf("#%d: First node is not an action: %s", i, test.input) 1551 continue 1552 } 1553 pipe := action.Pipe 1554 ensurePipelineContains(pipe, test.ids) 1555 got := pipe.String() 1556 if got != test.output { 1557 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got) 1558 } 1559 } 1560 } 1561 1562 func TestEscapeErrorsNotIgnorable(t *testing.T) { 1563 var b bytes.Buffer 1564 tmpl, _ := New("dangerous").Parse("<a") 1565 err := tmpl.Execute(&b, nil) 1566 if err == nil { 1567 t.Errorf("Expected error") 1568 } else if b.Len() != 0 { 1569 t.Errorf("Emitted output despite escaping failure") 1570 } 1571 } 1572 1573 func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1574 var b bytes.Buffer 1575 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1576 if err != nil { 1577 t.Errorf("failed to parse set: %q", err) 1578 } 1579 err = tmpl.ExecuteTemplate(&b, "t", nil) 1580 if err == nil { 1581 t.Errorf("Expected error") 1582 } else if b.Len() != 0 { 1583 t.Errorf("Emitted output despite escaping failure") 1584 } 1585 } 1586 1587 func TestRedundantFuncs(t *testing.T) { 1588 inputs := []interface{}{ 1589 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1590 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1591 ` !"#$%&'()*+,-./` + 1592 `0123456789:;<=>?` + 1593 `@ABCDEFGHIJKLMNO` + 1594 `PQRSTUVWXYZ[\]^_` + 1595 "`abcdefghijklmno" + 1596 "pqrstuvwxyz{|}~\x7f" + 1597 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 1598 "&%22\\", 1599 CSS(`a[href =~ "//example.com"]#foo`), 1600 HTML(`Hello, <b>World</b> &tc!`), 1601 HTMLAttr(` dir="ltr"`), 1602 JS(`c && alert("Hello, World!");`), 1603 JSStr(`Hello, World & O'Reilly\x21`), 1604 URL(`greeting=H%69&addressee=(World)`), 1605 } 1606 1607 for n0, m := range redundantFuncs { 1608 f0 := funcMap[n0].(func(...interface{}) string) 1609 for n1 := range m { 1610 f1 := funcMap[n1].(func(...interface{}) string) 1611 for _, input := range inputs { 1612 want := f0(input) 1613 if got := f1(want); want != got { 1614 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 1615 } 1616 } 1617 } 1618 } 1619 } 1620 1621 func TestIndirectPrint(t *testing.T) { 1622 a := 3 1623 ap := &a 1624 b := "hello" 1625 bp := &b 1626 bpp := &bp 1627 tmpl := Must(New("t").Parse(`{{.}}`)) 1628 var buf bytes.Buffer 1629 err := tmpl.Execute(&buf, ap) 1630 if err != nil { 1631 t.Errorf("Unexpected error: %s", err) 1632 } else if buf.String() != "3" { 1633 t.Errorf(`Expected "3"; got %q`, buf.String()) 1634 } 1635 buf.Reset() 1636 err = tmpl.Execute(&buf, bpp) 1637 if err != nil { 1638 t.Errorf("Unexpected error: %s", err) 1639 } else if buf.String() != "hello" { 1640 t.Errorf(`Expected "hello"; got %q`, buf.String()) 1641 } 1642 } 1643 1644 // This is a test for issue 3272. 1645 func TestEmptyTemplate(t *testing.T) { 1646 page := Must(New("page").ParseFiles(os.DevNull)) 1647 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 1648 t.Fatal("expected error") 1649 } 1650 } 1651 1652 type Issue7379 int 1653 1654 func (Issue7379) SomeMethod(x int) string { 1655 return fmt.Sprintf("<%d>", x) 1656 } 1657 1658 // This is a test for issue 7379: type assertion error caused panic, and then 1659 // the code to handle the panic breaks escaping. It's hard to see the second 1660 // problem once the first is fixed, but its fix is trivial so we let that go. See 1661 // the discussion for issue 7379. 1662 func TestPipeToMethodIsEscaped(t *testing.T) { 1663 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) 1664 tryExec := func() string { 1665 defer func() { 1666 panicValue := recover() 1667 if panicValue != nil { 1668 t.Errorf("panicked: %v\n", panicValue) 1669 } 1670 }() 1671 var b bytes.Buffer 1672 tmpl.Execute(&b, Issue7379(0)) 1673 return b.String() 1674 } 1675 for i := 0; i < 3; i++ { 1676 str := tryExec() 1677 const expect = "<html><0></html>\n" 1678 if str != expect { 1679 t.Errorf("expected %q got %q", expect, str) 1680 } 1681 } 1682 } 1683 1684 func BenchmarkEscapedExecute(b *testing.B) { 1685 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 1686 var buf bytes.Buffer 1687 b.ResetTimer() 1688 for i := 0; i < b.N; i++ { 1689 tmpl.Execute(&buf, "foo & 'bar' & baz") 1690 buf.Reset() 1691 } 1692 }