/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.template.soy.parsepasses.contextautoesc;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.template.soy.SoyFileSetParser.ParseResult;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.base.internal.SoyFileSupplier;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SanitizedContentOperator;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.error.FormattingErrorReporter;
import com.google.template.soy.shared.restricted.SoyPrintDirective;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import junit.framework.ComparisonFailure;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class ContextualAutoescaperTest {
/** Custom print directives used in tests below. */
private static final ImmutableMap<String, SoyPrintDirective> SOY_PRINT_DIRECTIVES =
ImmutableMap.of(
"|customEscapeDirective",
new SoyPrintDirective() {
@Override
public String getName() {
return "|customEscapeDirective";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(0);
}
@Override
public boolean shouldCancelAutoescape() {
return true;
}
},
"|customOtherDirective",
new SoyPrintDirective() {
@Override
public String getName() {
return "|customOtherDirective";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(0);
}
@Override
public boolean shouldCancelAutoescape() {
return false;
}
},
"|noAutoescape",
new SoyPrintDirective() {
@Override
public String getName() {
return "|noAutoescape";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(0);
}
@Override
public boolean shouldCancelAutoescape() {
return true;
}
},
"|bidiSpanWrap", new FakeBidiSpanWrapDirective());
@Test
public void testStrictModeIsDefault() {
assertRewriteFails(
"In file no-path:5:4, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"html\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main}\n",
" {@param foo: ?}\n",
"<b>{$foo|noAutoescape}</b>\n",
"{/template}"));
}
@Test
public void testTrivialTemplate() throws Exception {
assertContextualRewriting(
join("{namespace ns}\n\n", "{template .foo}\n", "Hello, World!\n", "{/template}"),
join("{namespace ns}\n\n", "{template .foo}\n", "Hello, World!\n", "{/template}"));
}
@Test
public void testPrintInText() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello, {$world |escapeHtml}!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello, {$world}!\n",
"{/template}"));
}
@Test
public void testPrivateTemplate() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .privateFoo autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param world: ?}\n",
"Hello, {$world |escapeHtml}!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .privateFoo autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param world: ?}\n",
"Hello, {$world}!\n",
"{/template}"));
}
@Test
public void testPrintInTextAndLink() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello,",
"<a href='worlds?world={$world |escapeUri}'>",
"{$world |escapeHtml}",
"</a>!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello,\n",
"<a href='worlds?world={$world}'>\n",
"{$world}\n",
"</a>!\n",
"{/template}\n"));
}
@Test
public void testObscureUrlAttributes() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
//"<meta http-equiv=refresh content='{$x |filterNormalizeUri |escapeHtmlAttribute}'>",
"<a xml:base='{$x |filterNormalizeUri |escapeHtmlAttribute}' href='/foo'>link</a>",
"<button formaction='{$x |filterNormalizeUri |escapeHtmlAttribute}'>do</button>",
"<command icon='{$x |filterNormalizeUri |escapeHtmlAttribute}'></command>",
"<object data='{$x |filterNormalizeUri |escapeHtmlAttribute}'></object>",
"<video poster='{$x |filterNormalizeUri |escapeHtmlAttribute}'></video>",
"<video src='{$x |filterNormalizeUri |escapeHtmlAttribute}'></video>",
"<source src='{$x |filterNormalizeUri |escapeHtmlAttribute}'>",
"<audio src='{$x |filterNormalizeUri |escapeHtmlAttribute}'></audio>",
"<script src='{$x |filterTrustedResourceUri |escapeHtmlAttribute}'",
"></script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
// TODO(msamuel): Re-enable content since it is often (but often not) used to convey
// URLs in place of <link rel> once we can figure out a good way to distinguish the
// URL use-cases from others.
//"<meta http-equiv=refresh content='{$x}'>\n",
"<a xml:base='{$x}' href='/foo'>link</a>\n",
"<button formaction='{$x}'>do</button>\n",
"<command icon='{$x}'></command>\n",
"<object data='{$x}'></object>\n",
"<video poster='{$x}'></video>\n",
"<video src='{$x}'></video>\n",
"<source src='{$x}'>\n",
"<audio src='{$x}'></audio>\n",
"<script src='{$x}'></script>",
"{/template}\n"));
}
@Test
public void testConditional() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,",
"{if $x == 1}",
"{$y |escapeHtml}",
"{elseif $x == 2}",
"<script>foo({$z |escapeJsValue})</script>",
"{else}",
"World!",
"{/if}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,\n",
"{if $x == 1}\n",
" {$y}\n",
"{elseif $x == 2}\n",
" <script>foo({$z})</script>\n",
"{else}\n",
" World!\n",
"{/if}\n",
"{/template}"));
}
@Test
public void testConditionalEndsInDifferentContext() throws Exception {
// Make sure that branches that ends in consistently different contexts transition to
// that different context.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param url: ?}\n",
" {@param name: ?}\n",
" {@param value: ?}\n",
"<a",
"{if $url}",
" href='{$url |filterNormalizeUri |escapeHtmlAttribute}'>",
"{elseif $name}",
" name='{$name |escapeHtmlAttribute}'>",
"{else}",
">",
"{/if}",
" onclick='alert({$value |escapeHtml})'\n", // Not escapeJsValue.
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param url: ?}\n",
" {@param name: ?}\n",
" {@param value: ?}\n",
"<a",
// Each of these branches independently closes the tag.
"{if $url}",
" href='{$url}'>",
"{elseif $name}",
" name='{$name}'>",
"{else}",
">",
"{/if}",
// So now make something that looks like a script attribute but which actually
// appears in a PCDATA. If the context merge has properly happened is is escaped as
// PCDATA.
" onclick='alert({$value})'\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .bar}\n",
" {@param p: ?}\n",
"<input{if $p} disabled{/if}>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .bar}\n",
" {@param p: ?}\n",
" {@param p2: ?}\n",
"<input{if $p} disabled{/if}{if $p2} checked{/if}>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .bar}\n",
" {@param p: ?}\n",
" {@param p2: ?}\n",
"<input {if $p}disabled{/if}{if $p2} checked{/if}>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .good4}\n",
" {@param p: ?}\n",
"<div{if $p} x=x{/if} x=y>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .good4}\n",
" {@param p: ?}\n",
"<div {if $p}onclick=foo() {/if} x=y>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .good4}\n",
" {@param p: ?}\n",
"<div foo=bar {if $p}onclick=foo() {/if} x=y>\n",
"{/template}"));
assertContextualRewriting(
"{namespace ns}\n\n"
+ "{template .good4}\n"
+ " {@param x: ?}\n"
+ "<input{if $x} onclick={$x |escapeJsValue |escapeHtmlAttributeNospace}{/if}>\n"
+ "{/template}",
join(
"{namespace ns}\n\n",
"{template .good4}\n",
" {@param x: ?}\n",
"\n" + "<input{if $x} onclick={$x}{/if}>\n",
"{/template}"));
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .good4}\n",
" {@param p: ?}\n",
"<input {if $p}disabled=\"true\"{/if}>",
"<input {if $p}onclick=\"foo()\"{/if}>\n",
"{/template}"));
}
@Test
public void testBrokenConditional() throws Exception {
assertRewriteFails(
"In file no-path:10:1, template ns.bar: "
+ "{if} command branch ends in a different context than preceding branches: "
+ "{elseif $x == 2}<script>foo({$z})//</scrpit>",
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,\n",
"{if $x == 1}\n",
" {$y}\n",
"{elseif $x == 2}\n",
" <script>foo({$z})//</scrpit>\n", // Not closed so ends inside JS.
"{else}\n",
" World!\n",
"{/if}\n",
"{/template}"));
}
@Test
public void testSwitch() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,",
"{switch $x}",
"{case 1}",
"{$y |escapeHtml}",
"{case 2}",
"<script>foo({$z |escapeJsValue})</script>",
"{default}",
"World!",
"{/switch}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,\n",
"{switch $x}\n",
" {case 1}\n",
" {$y}\n",
" {case 2}\n",
" <script>foo({$z})</script>\n",
" {default}\n",
" World!\n",
"{/switch}\n",
"{/template}"));
}
@Test
public void testBrokenSwitch() throws Exception {
assertRewriteFails(
"In file no-path:11:3, template ns.bar: "
+ "{switch} command case ends in a different context than preceding cases: "
+ "{case 2}<script>foo({$z})//</scrpit>",
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"Hello,\n",
"{switch $x}\n",
" {case 1}\n",
" {$y}\n",
" {case 2}\n",
// Not closed so ends inside JS
" <script>foo({$z})//</scrpit>\n",
" {default}\n",
" World!\n",
"{/switch}\n",
"{/template}"));
}
@Test
public void testPrintInsideScript() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param a: ?}\n",
" {@param b: ?}\n",
" {@param c: ?}\n",
" {@param d: ?}\n",
" {@param e: ?}\n",
" {@param f: ?}\n",
"<script>",
"foo({$a |escapeJsValue}); ",
"bar(\"{$b |escapeJsString}\"); ",
"baz(\'{$c |escapeJsString}\'); ",
"boo(/{$d |escapeJsRegex}/.test(s) ? 1 / {$e |escapeJsValue}",
" : /{$f |escapeJsRegex}/);",
"</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param a: ?}\n",
" {@param b: ?}\n",
" {@param c: ?}\n",
" {@param d: ?}\n",
" {@param e: ?}\n",
" {@param f: ?}\n",
"<script>\n",
"foo({$a});\n",
"bar(\"{$b}\");\n",
"baz(\'{$c}\');\n",
"boo(/{$d}/.test(s) ? 1 / {$e} : /{$f}/);\n",
"</script>\n",
"{/template}"));
}
@Test
public void testPrintInsideJsCommentRejected() throws Exception {
assertRewriteFails(
"In file no-path:5:12, template ns.foo: " + "JS comments cannot contain dynamic values.",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
// NOTE: Lack of whitespace before "//" makes sure it's not interpreted as a Soy
// comment.
"<script>// {$x}</script>\n",
"{/template}"));
}
@Test
public void testJsStringInsideQuotesRejected() throws Exception {
assertRewriteFails(
"In file no-path:5:22, template ns.foo: "
+ "Escaping modes [ESCAPE_JS_VALUE] not compatible with"
+ " (Context JS_SQ_STRING) : {$world |escapeJsValue}",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"<script>alert('Hello {$world |escapeJsValue}');</script>\n",
"{/template}"));
}
@Test
public void testLiteral() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
"<script>",
"{lb}$a{rb}",
"</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
"<script>\n",
"{literal}{$a}{/literal}\n",
"</script>\n",
"{/template}"));
}
@Test
public void testForLoop() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param n: ?}\n",
"<style>",
"{for $i in range($n)}",
".foo{$i |filterCssValue}:before {lb}",
"content: '{$i |escapeCssString}'",
"{rb}",
"{/for}",
"</style>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param n: ?}\n",
"<style>\n",
"{for $i in range($n)}\n",
" .foo{$i}:before {lb}\n",
" content: '{$i}'\n",
" {rb}\n",
"{/for}",
"</style>\n",
"{/template}"));
}
@Test
public void testBrokenForLoop() throws Exception {
assertRewriteFails(
"In file no-path:6:5, template ns.bar: "
+ "{for} command changes context so it cannot be reentered : "
+ "{for $i in range($n)}.foo{$i |filterCssValue}:before "
+ "{lb}content: '{$i |escapeCssString}{rb}{/for}",
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param n: ?}\n",
" <style>\n",
" {for $i in range($n)}\n",
" .foo{$i |filterCssValue}:before {lb}\n",
" content: '{$i |escapeCssString}\n", // Missing close quote.
" {rb}\n",
" {/for}\n",
" </style>\n",
"{/template}"));
}
@Test
public void testForeachLoop() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .baz autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
"<ol>",
"{foreach $x in $foo}",
"<li>{$x |escapeHtml}</li>",
"{/foreach}",
"</ol>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .baz autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
" <ol>\n",
" {foreach $x in $foo}\n",
" <li>{$x}</li>\n",
" {/foreach}\n",
" </ol>\n",
"{/template}"));
}
@Test
public void testBrokenForeachLoop() throws Exception {
assertRewriteFails(
"In file no-path:6:5, template ns.baz: "
+ "{foreach} body changes context : "
+ "{foreach $x in $foo}<li class={$x}{/foreach}",
join(
"{namespace ns}\n\n",
"{template .baz autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
" <ol>\n",
" {foreach $x in $foo}\n",
" <li class={$x}\n",
" {/foreach}\n",
" </ol>\n",
"{/template}"));
}
@Test
public void testForeachLoopWithIfempty() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .baz autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
"<ol>",
"{foreach $x in $foo}",
"<li>{$x |escapeHtml}</li>",
"{ifempty}",
"<li><i>Nothing</i></li>",
"{/foreach}",
"</ol>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .baz autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
" <ol>\n",
" {foreach $x in $foo}\n",
" <li>{$x}</li>\n",
" {ifempty}\n",
" <li><i>Nothing</i></li>\n",
" {/foreach}\n",
" </ol>\n",
"{/template}"));
}
@Test
public void testCall() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"{call .bar data=\"all\" /}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello, {$world |escapeHtml}!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
" {call .bar data=\"all\" /}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
" Hello, {$world}!\n",
"{/template}"));
}
@Test
public void testCallWithParams() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param? x: ?}\n",
"{call .bar}{param world: $x + 1 /}{/call}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param? world: ?}\n",
"Hello, {$world |escapeHtml}!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param? x: ?}\n",
"{call .bar}{param world: $x + 1 /}{/call}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param? world: ?}\n",
"Hello, {$world}!\n",
"{/template}"));
}
@Test
public void testSameTemplateCalledInDifferentContexts() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"{call .bar data=\"all\" /}",
"<script>",
"alert('{call ns.bar__C15 data=\"all\" /}');",
"</script>\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello, {$world |escapeHtml}!\n",
"{/template}\n\n",
"{template .bar__C15 autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
"Hello, {$world |escapeJsString}!\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
" {call .bar data=\"all\" /}\n",
" <script>\n",
" alert('{call .bar data=\"all\" /}');\n",
" </script>\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param world: ?}\n",
" Hello, {$world}!\n",
"{/template}"));
}
@Test
public void testRecursiveTemplateGuessWorks() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"<script>",
"x = [{call ns.countDown__C4011 data=\"all\" /}]",
"</script>\n",
"{/template}\n\n",
"{template .countDown autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"{if $x > 0}",
"{print --$x |escapeHtml},",
"{call .countDown}{param x : $x - 1 /}{/call}",
"{/if}\n",
"{/template}\n\n",
"{template .countDown__C4011 autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"{if $x > 0}",
"{print --$x |escapeJsValue},",
"{call ns.countDown__C4011}{param x : $x - 1 /}{/call}",
"{/if}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" <script>\n",
" x = [{call .countDown data=\"all\" /}]\n",
" </script>\n",
"{/template}\n\n",
"{template .countDown autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {if $x > 0}{print --$x},{call .countDown}{param x : $x - 1 /}{/call}{/if}\n",
"{/template}"));
}
@Test
public void testTemplateWithUnknownJsSlash() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param declare: ?}\n",
"<script>",
"{if $declare}var {/if}",
"x = {call ns.bar__C4011 /}{\\n}",
"y = 2",
" </script>\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param? declare: ?}\n",
"42",
"{if $declare}",
" , ",
"{/if}\n",
"{/template}\n\n",
"{template .bar__C4011 autoescape=\"deprecated-contextual\"}\n",
" {@param? declare: ?}\n",
"42",
"{if $declare}",
" , ",
"{/if}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param declare: ?}\n",
" <script>\n",
" {if $declare}var{sp}{/if}\n",
" x = {call .bar /}{\\n}\n",
// At this point we don't know whether or not a slash would start
// a RegExp or not, but we don't see a slash so it doesn't matter.
" y = 2",
" </script>\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param? declare: ?}\n",
// A slash following 42 would be a division operator.
" 42\n",
// But a slash following a comma would be a RegExp.
" {if $declare} , {/if}\n", //
"{/template}"));
}
@Test
public void testTemplateUnknownJsSlashMatters() throws Exception {
assertRewriteFails(
"In file no-path:8:5, template ns.foo: "
+ "Slash (/) cannot follow the preceding branches since it is unclear whether the slash"
+ " is a RegExp literal or division operator."
+ " Please add parentheses in the branches leading to `/ 2 </script>`",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param? declare : ?}\n",
" <script>\n",
" {if $declare}var{sp}{/if}\n",
" x = {call .bar /}\n",
// At this point we don't know whether or not a slash would start
// a RegExp or not, so this constitutes an error.
" / 2",
" </script>\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param? declare : ?}\n",
// A slash following 42 would be a division operator.
" 42\n",
// But a slash following a comma would be a RegExp.
" {if $declare} , {/if}\n", //
"{/template}"));
}
@Test
public void testUrlContextJoining() throws Exception {
// This is fine. The ambiguity about
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param c: ?}\n",
"<a href=\"",
"{if $c}",
"/foo?bar=baz",
"{else}",
"/boo",
"{/if}",
"\">\n",
"{/template}"));
assertRewriteFails(
"In file no-path:6:50, template ns.foo: Cannot determine which part of the URL this dynamic"
+ " value is in. Most likely, a preceding conditional block began a ?query or "
+ "#fragment, but only on one branch.",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param c: ?}\n",
"<a href=\"",
"{if $c}",
"/foo?bar=baz&boo=",
"{else}",
"/boo/",
"{/if}",
"{$x}",
"\">\n",
"{/template}"));
}
@Test
public void testUrlMaybeVariableSchemePrintStatement() throws Exception {
assertRewriteFails(
"In file no-path:6:14, template ns.foo: Soy can't prove this URI concatenation has a safe"
+ " scheme at compile time. Either combine adjacent print statements (e.g. {$x + $y}"
+ " instead of {$x}{$y}), or introduce disambiguating characters"
+ " (e.g. {$x}/{$y}, {$x}?y={$y}, {$x}&y={$y}, {$x}#{$y})",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
"<a href=\"{$x}{$y}\">Test</a>\n",
"{/template}"));
}
@Test
public void testUrlMaybeVariableSchemeColon() throws Exception {
assertRewriteFails(
"In file no-path:5:14, template ns.foo: Soy can't safely process a URI that might start "
+ "with a variable scheme. For example, {$x}:{$y} could have an XSS if $x is "
+ "'javascript' and $y is attacker-controlled. Either use a hard-coded scheme, or "
+ "introduce disambiguating characters (e.g. http://{$x}:{$y}, ./{$x}:{$y}, "
+ "or {$x}?foo=:{$y})",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"{$x}:foo()\">Test</a>\n",
"{/template}"));
}
@Test
public void testUrlMaybeSchemePrintStatement() throws Exception {
assertRewriteFails(
"In file no-path:5:13, template ns.foo:"
+ " Soy can't prove this URI has a safe scheme at compile time. Either make sure one of"
+ " ':', '/', '?', or '#' comes before the dynamic value (e.g. foo/{$bar}), or move the"
+ " print statement to the start of the URI to enable runtime validation"
+ " (e.g. href=\"{'foo' + $bar}\" instead of href=\"foo{$bar}\").",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"foo{$x}\">Test</a>\n",
"{/template}"));
}
@Test
public void testUrlDangerousSchemeForbidden() throws Exception {
String message =
"Soy can't properly escape for this URI scheme. For image sources, you can print full"
+ " data and blob URIs directly (e.g. src=\"{$someDataUri}\")."
+ " Otherwise, hardcode the full URI in the template or pass a complete"
+ " SanitizedContent or SafeUri object.";
assertRewriteFails(
"In file no-path:5:26, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"javas{nil}cript:{$x}\">\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:29, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<style>url('javas{nil}cript:{$x}')</style>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:24, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<style>url(\"javascript:{$x}\")</style>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:30, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<style>url(\"javascript:alert({$x})\")</style>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:23, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<style>url(javascript:{$x})</style>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:17, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<style>url(data:{$x})</style>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:15, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"data:{$x}\">\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:15, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"blob:{$x}\">\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:21, template ns.foo: " + message,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"filesystem:{$x}\">\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"not-javascript:{$x |escapeHtmlAttribute}\">Test</a>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"not-javascript:{$x}\">Test</a>\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"javascript-foo:{$x |escapeHtmlAttribute}\">Test</a>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"javascript-foo:{$x}\">Test</a>\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"not?javascript:{$x |escapeUri}\">Test</a>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<a href=\"not?javascript:{$x}\">Test</a>\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
"<a href=\"javascript:hardcoded()\">Test</a>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
"<a href=\"javascript:hardcoded()\">Test</a>\n",
"{/template}"));
}
@Test
public void testRecursiveTemplateGuessFails() throws Exception {
assertRewriteFails(
"In file no-path:5:5, template ns.foo: Error while re-contextualizing template ns.quot in"
+ " context (Context JS REGEX):"
+ "\n- In file no-path:10:27, template ns.quot__C4011: Error while re-contextualizing"
+ " template ns.quot in context (Context JS_DQ_STRING):"
+ "\n- In file no-path:10:5, template ns.quot__C14: {if} command without {else} changes"
+ " context : {if randomInt(10) < 5}{call .quot data=\"all\" /}{/if}",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" <script>\n",
" {call .quot data=\"all\" /}\n",
" </script>\n",
"{/template}\n\n",
"{template .quot autoescape=\"deprecated-contextual\"}\n",
" \" {if randomInt(10) < 5}{call .quot data=\"all\" /}{/if}\n",
"{/template}"));
}
@Test
public void testUris() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param url: ?}\n",
" {@param bgimage: ?}\n",
" {@param anchor: ?}\n",
" {@param file: ?}\n",
" {@param brdr: ?}\n",
// We use filterNormalizeUri at the beginning,
"<a href='{$url |filterNormalizeUri |escapeHtmlAttribute}'",
" style='background:url({$bgimage |filterNormalizeMediaUri |escapeHtmlAttribute})'>",
"Hi</a>",
"<a href='#{$anchor |escapeHtmlAttribute}'",
// escapeUri for substitutions into queries.
" style='background:url('/pic?q={$file |escapeUri}')'>",
"Hi",
"</a>",
"<style>",
"body {lb} background-image: url(\"{$bgimage |filterNormalizeMediaUri}\"); {rb}",
// and normalizeUri without the filter in the path.
"table {lb} border-image: url(\"borders/{$brdr |normalizeUri}\"); {rb}",
"</style>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .bar autoescape=\"deprecated-contextual\"}\n",
" {@param url: ?}\n",
" {@param bgimage: ?}\n",
" {@param anchor: ?}\n",
" {@param file: ?}\n",
" {@param brdr: ?}\n",
"<a href='{$url}' style='background:url({$bgimage})'>Hi</a>\n",
"<a href='#{$anchor}'\n",
" style='background:url('/pic?q={$file}')'>Hi</a>\n",
"<style>\n",
"body {lb} background-image: url(\"{$bgimage}\"); {rb}\n",
"table {lb} border-image: url(\"borders/{$brdr}\"); {rb}\n",
"</style>\n",
"{/template}"));
}
@Test
public void testTrustedResourceUri() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param start: ?}\n",
" {@param path: ?}\n",
" {@param query: ?}\n",
" {@param fragment: ?}\n",
"<script src='{$start |filterTrustedResourceUri ",
"|escapeHtmlAttribute}/{$path |filterTrustedResourceUri |escapeHtmlAttribute}?",
"q={$query |filterTrustedResourceUri |escapeUri}#{$fragment |filterTrustedResourceUri ",
"|escapeHtmlAttribute}'></script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param start: ?}\n",
" {@param path: ?}\n",
" {@param query: ?}\n",
" {@param fragment: ?}\n",
"<script src='{$start}/{$path}?q={$query}#{$fragment}'></script>",
"{/template}\n"));
}
@Test
public void testBlessStringAsTrustedResourceUrlForLegacy() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param start: ?}\n",
" {@param path: ?}\n",
" {@param query: ?}\n",
" {@param fragment: ?}\n",
"<script src='",
"{$start |blessStringAsTrustedResourceUrlForLegacy ",
"|filterNormalizeUri |escapeHtmlAttribute}",
"/{$path |blessStringAsTrustedResourceUrlForLegacy ",
"|escapeHtmlAttribute}",
"?q={$query |blessStringAsTrustedResourceUrlForLegacy ",
"|escapeUri}",
"#{$fragment |blessStringAsTrustedResourceUrlForLegacy ",
"|escapeHtmlAttribute}'></script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param start: ?}\n",
" {@param path: ?}\n",
" {@param query: ?}\n",
" {@param fragment: ?}\n",
"<script src='{$start |blessStringAsTrustedResourceUrlForLegacy}",
"/{$path |blessStringAsTrustedResourceUrlForLegacy}",
"?q={$query |blessStringAsTrustedResourceUrlForLegacy}",
"#{$fragment |blessStringAsTrustedResourceUrlForLegacy}'></script>",
"{/template}\n"));
}
@Test
public void testCss() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"{css foo}\n",
"{/template}"));
}
@Test
public void testXid() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"{xid foo}\n",
"{/template}"));
}
@Test
public void testAlreadyEscaped() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param FOO: ?}\n",
"<script>a = \"{$FOO |escapeUri}\";</script>\n",
"{/template}"));
}
@Test
public void testExplicitNoescapeNoop() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param FOO: ?}\n",
"<script>a = \"{$FOO |noAutoescape}\";</script>\n",
"{/template}"));
}
@Test
public void testCustomDirectives() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
"{$x |customEscapeDirective} - {$y |customOtherDirective |escapeHtml}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {$x |customEscapeDirective} - {$y |customOtherDirective}\n",
"{/template}"));
}
@Test
public void testNoInterferenceWithNonContextualTemplates() throws Exception {
// If a broken template ns.calls a contextual template, object.
assertRewriteFails(
null,
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"{@param? world: ?}\n",
" Hello {$world}\n",
"{/template}\n\n",
"{template .bad autoescape=\"deprecated-noncontextual\"}\n",
"{@param x: ?}\n",
" {if $x}\n",
" <!--\n",
" {/if}\n",
" {call .foo/}\n",
"{/template}"));
// But if it doesn't, it's none of our business.
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param? world: ?}\n",
"Hello {$world |escapeHtml}\n",
"{/template}\n\n",
"{template .bad autoescape=\"deprecated-noncontextual\"}\n",
" {@param x: ?}\n",
"{if $x}",
"<!--",
"{/if}\n",
// No call to foo in this version.
"{/template}"));
}
@Test
public void testExternTemplates() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script>",
"var x = {call .bar /},", // Not defined in this compilation unit.
"y = {$y |escapeJsValue};",
"</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script>",
"var x = {call .bar /},", // Not defined in this compilation unit.
"y = {$y};",
"</script>\n",
"{/template}"));
}
@Test
public void testNonContextualCallers() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param? x: ?}\n",
"{$x |escapeHtml}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-noncontextual\"}\n",
" {@param y: ?}\n",
"<b>{call .foo /}</b> {$y |escapeHtml}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param? x: ?}\n",
"{$x}\n",
"{/template}\n\n",
"{template .bar autoescape=\"deprecated-noncontextual\"}\n",
" {@param y: ?}\n",
"<b>{call .foo /}</b> {$y}\n",
"{/template}"));
}
@Test
public void testUnquotedAttributes() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param msg: ?}\n",
"<button onclick=alert({$msg |escapeJsValue |escapeHtmlAttributeNospace})>",
"Launch</button>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param msg: ?}\n",
"<button onclick=alert({$msg})>Launch</button>\n",
"{/template}"));
}
@Test
public void testMessagesWithEmbeddedTags() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"{msg desc=\"Say hello\"}Hello, <b>World</b>{/msg}\n",
"{/template}"));
}
@Test
public void testNamespaces() throws Exception {
// Test calls in namespaced files.
assertContextualRewriting(
join(
"{namespace soy.examples.codelab}\n\n",
"/** */\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
"<title>{call soy.examples.codelab.pagenum__C81 data=\"all\" /}</title>",
"",
"<script>",
"var pagenum = \"{call soy.examples.codelab.pagenum__C14 data=\"all\" /}\"; ",
"...",
"</script>\n",
"{/template}\n\n",
"/**\n",
" * @param pageIndex 0-indexed index of the current page.\n",
" * @param pageCount Total count of pages. Strictly greater than pageIndex.\n",
" */\n",
"{template .pagenum autoescape=\"deprecated-contextual\" private=\"true\"}\n",
"{$pageIndex |escapeHtml} of {$pageCount |escapeHtml}\n",
"{/template}\n\n",
"{template .pagenum__C81 autoescape=\"deprecated-contextual\" private=\"true\"}\n",
"{$pageIndex |escapeHtmlRcdata} of {$pageCount |escapeHtmlRcdata}\n",
"{/template}\n\n",
"{template .pagenum__C14 autoescape=\"deprecated-contextual\" private=\"true\"}\n",
"{$pageIndex |escapeJsString} of {$pageCount |escapeJsString}\n",
"{/template}"),
join(
"{namespace soy.examples.codelab}\n\n",
"/** */\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" <title>{call .pagenum data=\"all\" /}</title>\n",
" <script>\n",
" var pagenum = \"{call .pagenum data=\"all\" /}\";\n",
" ...\n",
" </script>\n",
"{/template}\n\n",
"/**\n",
" * @param pageIndex 0-indexed index of the current page.\n",
" * @param pageCount Total count of pages. Strictly greater than pageIndex.\n",
" */\n",
"{template .pagenum autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {$pageIndex} of {$pageCount}\n",
"{/template}"));
}
@Test
public void testConditionalAttributes() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param className: ?}\n",
"<div{if $className} class=\"{$className |escapeHtmlAttribute}\"{/if}>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param className: ?}\n",
"<div{if $className} class=\"{$className}\"{/if}>\n",
"{/template}"));
}
@Test
public void testExtraSpacesInTag() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param className: ?}\n",
"<div {if $className} class=\"{$className |escapeHtmlAttribute}\"{/if} id=x>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param className: ?}\n",
"<div {if $className} class=\"{$className}\"{/if} id=x>\n",
"{/template}"));
}
@Test
public void testOptionalAttributes() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .icontemplate autoescape=\"deprecated-contextual\"}\n",
" {@param iconId: ?}\n",
" {@param iconClass: ?}\n",
" {@param iconPath: ?}\n",
" {@param title: ?}\n",
" {@param alt: ?}\n",
"<img class=\"{$iconClass |escapeHtmlAttribute}\"",
"{if $iconId}",
" id=\"{$iconId |escapeHtmlAttribute}\"",
"{/if}",
" src=",
"{if $iconPath}",
"\"{$iconPath |filterNormalizeMediaUri |escapeHtmlAttribute}\"",
"{else}",
"\"images/cleardot.gif\"",
"{/if}",
"{if $title}",
" title=\"{$title |escapeHtmlAttribute}\"",
"{/if}",
" alt=\"",
"{if $alt or $alt == ''}",
"{$alt |escapeHtmlAttribute}",
"{elseif $title}",
"{$title |escapeHtmlAttribute}",
"{/if}\"",
">\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .icontemplate autoescape=\"deprecated-contextual\"}\n",
" {@param iconId: ?}\n",
" {@param iconClass: ?}\n",
" {@param iconPath: ?}\n",
" {@param title: ?}\n",
" {@param alt: ?}\n",
"<img class=\"{$iconClass}\"",
"{if $iconId}",
" id=\"{$iconId}\"",
"{/if}",
// Double quotes inside if/else.
" src=",
"{if $iconPath}",
"\"{$iconPath}\"",
"{else}",
"\"images/cleardot.gif\"",
"{/if}",
"{if $title}",
" title=\"{$title}\"",
"{/if}",
" alt=\"",
"{if $alt or $alt == ''}",
"{$alt}",
"{elseif $title}",
"{$title}",
"{/if}\"",
">\n",
"{/template}"));
}
@Test
public void testSvgImage() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .image autoescape=\"deprecated-contextual\"}\n",
" {@param iconPath: ?}\n",
"<svg>",
"<image xlink:href=\"{$iconPath |filterNormalizeMediaUri |escapeHtmlAttribute}\">",
"</svg>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .image autoescape=\"deprecated-contextual\"}\n",
" {@param iconPath: ?}\n",
"<svg>",
"<image xlink:href=\"{$iconPath}\">",
"</svg>\n",
"{/template}"));
}
@Test
public void testDynamicAttrName() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param baz: ?}\n",
"<img src=\"bar\" {$baz |filterHtmlAttributes}=\"boo\">\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param baz: ?}\n",
"<img src=\"bar\" {$baz}=\"boo\">\n",
"{/template}"));
}
@Test
public void testDynamicAttributes() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param baz: ?}\n",
"<img src=\"bar\" {$baz |filterHtmlAttributes}>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param baz: ?}\n",
"<img src=\"bar\" {$baz}>\n",
"{/template}"));
}
@Test
public void testDynamicAttributeValue() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x=x{$baz |escapeHtmlAttributeNospace}x>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x=x{$baz}x>\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x='x{$baz |escapeHtmlAttribute}x'>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x='x{$baz}x'>\n",
"{/template}"));
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x=\"x{$baz |escapeHtmlAttribute}x\">\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param baz: ?}\n",
"<img x=\"x{$baz}x\">\n",
"{/template}"));
}
@Test
public void testDynamicElementName() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param x: ?}\n",
"<{$x |filterHtmlElementName}>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo}\n",
" {@param x: ?}\n",
"<{$x}>\n",
"{/template}"));
}
@Test
public void testTagNameEdgeCases() {
assertRewriteFails(
"In file no-path:3:16, template ns.foo: "
+ "Saw unmatched close tag for context-changing tag: script",
join("{namespace ns}\n\n", "{template .foo}\n", "</script>\n", "{/template}"));
assertRewriteFails(
"In file no-path:3:16, template ns.foo: "
+ "Saw unmatched close tag for context-changing tag: xmp",
join("{namespace ns}\n\n", "{template .foo}\n", "</xmp>\n", "{/template}"));
assertRewriteFails(
"In file no-path:3:16, template ns.foo: Invalid end-tag name.",
join("{namespace ns}\n\n", "{template .foo}\n", "</3>\n", "{/template}"));
}
@Test
public void testOptionalValuelessAttributes() throws Exception {
assertContextualRewritingNoop(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"<input {if c}checked{/if}>",
"<input {if c}id={id |customEscapeDirective}{/if}>\n",
"{/template}"));
}
@Test
public void testDirectivesOrderedProperly() throws Exception {
// The |bidiSpanWrap directive takes HTML and produces HTML, so the |escapeHTML
// should appear first.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"{$x |escapeHtml |bidiSpanWrap}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"{$x |bidiSpanWrap}\n",
"{/template}"));
// But if we have a |bidiSpanWrap directive in a non HTML context, then don't reorder.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"<script>var html = {$x |bidiSpanWrap |escapeJsValue}</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
"<script>var html = {$x |bidiSpanWrap}</script>\n",
"{/template}"));
}
@Test
public void testDelegateTemplatesAreEscaped() throws Exception {
assertContextualRewriting(
join(
"{delpackage dp}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"{$x |escapeHtml}\n",
"{/deltemplate}"),
join(
"{delpackage dp}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"{$x}\n",
"{/deltemplate}"));
}
@Test
public void testDelegateTemplatesReturnTypesUnioned() throws Exception {
assertRewriteFails(
"In file no-path-0:7:1, template ns.main: "
+ "Slash (/) cannot follow the preceding branches since it is unclear whether the slash"
+ " is a RegExp literal or division operator. "
+ "Please add parentheses in the branches leading to "
+ "`/foo/i.test(s) && alert(s);</script>`",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
"{delcall ns.foo}\n",
"{param x: '' /}\n",
"{/delcall}\n",
// The / here is intended to start a regex, but if the version
// from dp2 is used it won't be.
"/foo/i.test(s) && alert(s);\n",
"</script>\n",
"{/template}"),
join(
"{delpackage dp1}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"<script>x = {$x};\n", // semicolon terminated
"{/deltemplate}"),
join(
"{delpackage dp2}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"<script>x = {$x}\n", // not semicolon terminated
"{/deltemplate}"));
}
@Test
public void testDelegateTemplatesMustHaveCompatibleEndContexts() throws Exception {
assertRewriteFails(
"In file no-path-0:4:1, template ns.main: "
+ "Error while re-contextualizing template ns.foo in context (Context HTML_PCDATA):\n"
+ "- In file no-path-1:5:1, template ns.foo: "
+ "Deltemplates diverge when used with deprecated-contextual autoescaping. "
+ "Based on the call site, assuming these templates all start in (Context HTML_PCDATA),"
+ " the different deltemplates end in incompatible contexts: "
+ "(Context JS REGEX), (Context HTML_PCDATA)",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
"{delcall ns.foo}\n",
"{param x: '' /}\n",
"{/delcall}\n",
"</script>\n",
"{/template}"),
join(
"{delpackage dp1}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"<script>x = {$x};\n",
"{/deltemplate}"),
join(
"{delpackage dp2}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"This does not open a script tag.\n",
"{/deltemplate}"));
}
@Test
public void testTypedLetBlockIsContextuallyEscaped() {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script> var y = '",
// Note that the contents of the {let} block are escaped in HTML PCDATA context, even
// though it appears in a JS string context in the template.
"{let $l kind=\"html\"}",
"<div>{$y |escapeHtml}</div>",
"{/let}",
"{$y |escapeJsString}'</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script> var y = '\n",
"{let $l kind=\"html\"}\n",
"<div>{$y}</div>",
"{/let}",
"{$y}'</script>\n",
"{/template}"));
}
@Test
public void testUntypedLetBlockIsContextuallyEscaped() {
// Test that the behavior for let blocks without kind attribute is unchanged (i.e., they are
// contextually escaped in the context the {let} command appears in).
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script> var y = '",
"{let $l}",
"<div>{$y |escapeJsString}</div>",
"{/let}",
"{$y |escapeJsString}'</script>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<script> var y = '\n",
"{let $l}\n",
"<div>{$y}</div>",
"{/let}",
"{$y}'</script>\n",
"{/template}"));
}
@Test
public void testTypedLetBlockIsStrictModeAutoescaped() {
assertRewriteFails(
"In file no-path:6:4, template ns.t: "
+ "Autoescape-cancelling print directives like |customEscapeDirective are only allowed "
+ "in kind=\"text\" blocks. If you really want to over-escape, try using a let block: "
+ "{let $foo kind=\"text\"}{$y |customEscapeDirective}{/let}{$foo}.",
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"{let $l kind=\"html\"}\n",
"<b>{$y |customEscapeDirective}</b>",
"{/let}\n",
"{/template}"));
assertRewriteFails(
"In file no-path:6:4, template ns.t: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"html\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
// Strict templates never allow noAutoescape.
"{template .t autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"{let $l kind=\"html\"}\n",
"<b>{$y |noAutoescape}</b>",
"{/let}\n",
"{/template}"));
assertRewriteFails(
"In file no-path:6:9, template ns.t: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"js\" or SanitizedContent.",
join(
// Throw in a red-herring namespace, just to check things.
"{namespace ns autoescape=\"deprecated-contextual\"}\n\n",
// Strict templates never allow noAutoescape.
"{template .t autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"{let $l kind=\"html\"}\n",
"<script>{$y |noAutoescape}</script>",
"{/let}\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:4, template ns.t: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{call .other data=\"all\" /}",
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
"{let $l kind=\"html\"}\n",
"<b>{call .other data=\"all\"/}</b>",
"{/let}\n",
"{/template}\n\n",
"{template .other autoescape=\"deprecated-contextual\"}\n",
"Hello World\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:4, template ns.t: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{call .other data=\"all\" /}",
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
"{let $l kind=\"html\"}\n",
"<b>{call .other data=\"all\"/}</b>",
"{/let}\n",
"{/template}\n\n",
"{template .other autoescape=\"deprecated-contextual\"}\n",
"Hello World\n",
"{/template}"));
// Non-autoescape-cancelling directives are allowed.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"{let $l kind=\"html\"}",
"<b>{$y |customOtherDirective |escapeHtml}</b>",
"{/let}\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .t autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"{let $l kind=\"html\"}\n",
"<b>{$y |customOtherDirective}</b>",
"{/let}\n",
"{/template}"));
}
@Test
public void testNonTypedParamMustEndInHtmlContextButWasAttribute() throws Exception {
assertRewriteFails(
"In file no-path:5:5, template ns.caller: "
+ "Blocks should start and end in HTML context: {param foo}",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {call .callee}\n",
" {param foo}<a href='{/param}\n",
" {/call}\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
" <b>{$foo}</b>\n",
"{/template}\n"));
}
@Test
public void testNonTypedParamMustEndInHtmlContextButWasScript() throws Exception {
assertRewriteFails(
"In file no-path:5:5, template ns.caller: "
+ "Blocks should start and end in HTML context: {param foo}",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {call .callee}\n",
" {param foo}<script>var x={/param}\n",
" {/call}\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\"}\n",
" {@param foo: ?}\n",
" <b>{$foo}</b>\n",
"{/template}\n"));
}
@Test
public void testNonTypedParamGetsContextuallyAutoescaped() throws Exception {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param query: ?}\n",
"{call .callee}",
"{param fooHtml}",
"<a href=\"http://google.com/search?q={$query |escapeUri}\" ",
"onclick=\"alert('{$query |escapeJsString |escapeHtmlAttribute}')\">",
"Search for {$query |escapeHtml}",
"</a>",
"{/param}",
"{/call}",
"\n{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\"}\n",
" {@param? fooHTML: ?}\n",
"{$fooHTML |noAutoescape}",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param query: ?}\n",
" {call .callee}\n",
" {param fooHtml}\n",
" <a href=\"http://google.com/search?q={$query}\"\n",
" onclick=\"alert('{$query}')\">\n",
" Search for {$query}\n",
" </a>\n",
" {/param}\n",
" {/call}\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\"}\n",
" {@param? fooHTML: ?}\n",
" {$fooHTML |noAutoescape}\n",
"{/template}"));
}
@Test
public void testTypedParamBlockIsContextuallyEscaped() {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}",
"<script> var y ='{$y |escapeJsString}';</script>",
"{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x |escapeHtml}</b>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}{param x kind=\"html\"}",
"<script> var y ='{$y}';</script>",
"{/param}{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
}
@Test
public void testTypedParamBlockIsStrictModeAutoescaped() {
assertRewriteFails(
"In file no-path:5:44, template ns.caller: "
+ "Autoescape-cancelling print directives like |customEscapeDirective are only allowed "
+ "in kind=\"text\" blocks. If you really want to over-escape, try using a let block: "
+ "{let $foo kind=\"text\"}{$y |customEscapeDirective}{/let}{$foo}.",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customEscapeDirective}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"strict\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// noAutoescape has a special error message.
assertRewriteFails(
"In file no-path:5:44, template ns.caller: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"html\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |noAutoescape}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"strict\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// NOTE: This error only works for non-extern templates.
assertRewriteFails(
"In file no-path:5:41, template ns.caller: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{call .subCallee data=\"all\" /}",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}{call .subCallee data=\"all\"/}{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"strict\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}\n\n",
"{template .subCallee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// Non-escape-cancelling directives are allowed.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customOtherDirective |escapeHtml}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"strict\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x |escapeHtml}</b>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"strict\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customOtherDirective}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"strict\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
}
@Test
public void testTransitionalTypedParamBlock() {
// In non-strict contextual templates, param blocks employ "transitional" strict autoescaping,
// which permits noAutoescape. This helps teams migrate the callees to strict even if not all
// the callers can be fixed.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |noAutoescape}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x |escapeHtml}</b>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |noAutoescape}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// Other escape-cancelling directives are still not allowed.
assertRewriteFails(
"In file no-path:5:44, template ns.caller: "
+ "Autoescape-cancelling print directives like |customEscapeDirective are only allowed "
+ "in kind=\"text\" blocks. If you really want to over-escape, try using a let block: "
+ "{let $foo kind=\"text\"}{$y |customEscapeDirective}{/let}{$foo}.",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customEscapeDirective}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// NOTE: This error only works for non-extern templates.
assertRewriteFails(
"In file no-path:4:41, template ns.caller: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{call .subCallee data=\"all\" /}",
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}{call .subCallee data=\"all\"/}{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}\n\n",
"{template .subCallee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
// Non-escape-cancelling directives are allowed.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customOtherDirective |escapeHtml}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x |escapeHtml}</b>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param y: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"html\"}<b>{$y |customOtherDirective}</b>{/param}",
"{/call}",
"</div>\n",
"{/template}\n\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
}
@Test
public void testTypedTextParamBlock() {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"<div>",
"{call .callee}",
"{param x kind=\"text\"}",
"Hello {$x |text} <{$y |text}, \"{$z |text}\">",
"{/param}",
"{/call}",
"</div>\n",
"{/template}\n",
"\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x |escapeHtml}</b>\n",
"{/template}"),
join(
"{namespace ns}\n\n",
"{template .caller autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"<div>",
"{call .callee}{param x kind=\"text\"}",
"Hello {$x} <{$y}, \"{$z}\">",
"{/param}{/call}",
"</div>\n",
"{/template}\n",
"\n",
"{template .callee autoescape=\"deprecated-contextual\" private=\"true\"}\n",
" {@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
}
@Test
public void testTypedTextLetBlock() {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"{let $a kind=\"text\"}",
"Hello {$x |text} <{$y |text}, \"{$z |text}\">",
"{/let}",
"{$a |escapeHtml}",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"{let $a kind=\"text\"}",
"Hello {$x} <{$y}, \"{$z}\">",
"{/let}",
"{$a}",
"\n{/template}"));
}
@Test
public void testStrictModeRejectsAutoescapeCancellingDirectives() {
assertRewriteFails(
"In file no-path:5:4, template ns.main: "
+ "Autoescape-cancelling print directives like |customEscapeDirective are only allowed "
+ "in kind=\"text\" blocks. If you really want to over-escape, try using a let block: "
+ "{let $foo kind=\"text\"}{$foo |customEscapeDirective}{/let}{$foo}.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<b>{$foo|customEscapeDirective}</b>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:4, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"html\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<b>{$foo|noAutoescape}</b>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:10, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"uri\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<a href=\"{$foo|noAutoescape}\">Test</a>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:6, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"attributes\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<div {$foo|noAutoescape}>Test</div>\n",
"{/template}"));
assertRewriteFails(
"In file no-path:5:9, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with kind=\"js\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<script>{$foo|noAutoescape}</script>\n",
"{/template}"));
// NOTE: There's no recommended context for textarea, since it's really essentially text.
assertRewriteFails(
"In file no-path:5:11, template ns.main: "
+ "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a {param} "
+ "with appropriate kind=\"...\" or SanitizedContent.",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<textarea>{$foo|noAutoescape}</textarea>\n",
"{/template}"));
}
@Test
public void testStrictModeRejectsNonStrictCalls() {
assertRewriteFails(
"In file no-path:4:4, template ns.main: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{call .bar data=\"all\" /}",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\" kind=\"html\"}\n",
"<b>{call .bar data=\"all\"/}\n",
"{/template}\n\n" + "{template .bar autoescape=\"deprecated-contextual\"}\n",
"Hello World\n",
"{/template}"));
assertRewriteFails(
"In file no-path-0:4:1, template ns.main: "
+ "Soy strict autoescaping currently forbids calls to non-strict templates, unless the "
+ "context is kind=\"text\", since there's no guarantee the callee is safe: "
+ "{delcall ns.foo}",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
"{delcall ns.foo}\n",
"{param x: '' /}\n",
"{/delcall}\n",
"{/template}"),
join(
"{delpackage dp1}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"<b>{$x}</b>\n",
"{/deltemplate}"),
join(
"{delpackage dp2}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"deprecated-contextual\"}\n",
"<i>{$x}</i>\n",
"{/deltemplate}"));
}
@Test
public void testContextualCannotCallStrictOfWrongContext() {
// Can't call a text template ns.from a strict context.
assertRewriteFails(
"In file no-path:4:1, template ns.main: "
+ "Cannot call strictly autoescaped template ns.foo of kind=\"text\" from incompatible "
+ "context (Context HTML_PCDATA). Strict templates generate extra code to safely call "
+ "templates of other content kinds, but non-strict templates do not: "
+ "{call .foo}",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
"{call .foo}\n",
"{param x: '' /}\n",
"{/call}\n",
"{/template}\n\n",
"{template .foo autoescape=\"strict\" kind=\"text\"}\n",
"{@param x: ?}\n",
"<b>{$x}</b>\n",
"{/template}"));
assertRewriteFails(
"In file no-path-0:4:1, template ns.main: "
+ "Cannot call strictly autoescaped template ns.foo of kind=\"text\" from incompatible "
+ "context (Context HTML_PCDATA). Strict templates generate extra code to safely call "
+ "templates of other content kinds, but non-strict templates do not: "
+ "{delcall ns.foo}",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
"{delcall ns.foo}\n",
"{param x: '' /}\n",
"{/delcall}\n",
"{/template}"),
join(
"{delpackage dp1}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"strict\" kind=\"text\"}\n",
"<b>{$x}</b>\n",
"{/deltemplate}"),
join(
"{delpackage dp2}\n",
"{namespace ns}\n\n",
"/** @param x */\n",
"{deltemplate ns.foo autoescape=\"strict\" kind=\"text\"}\n",
"<i>{$x}</i>\n",
"{/deltemplate}"));
}
@Test
public void testStrictModeAllowsNonAutoescapeCancellingDirectives() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<b>{$foo |customOtherDirective}</b>\n",
"{/template}"))
.parse()
.fileSet();
String rewrittenTemplate = rewrittenSource(soyTree);
assertThat(rewrittenTemplate.trim())
.isEqualTo(
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\"}\n",
" {@param foo: ?}\n",
"<b>{$foo |customOtherDirective |escapeHtml}</b>\n",
"{/template}"));
}
@Test
public void testStrictModeRequiresStartAndEndToBeCompatible() {
assertRewriteFails(
"In file no-path:3:1, template ns.main: "
+ "A strict block of kind=\"js\" cannot end in context (Context JS_SQ_STRING). "
+ "Likely cause is an unterminated string literal: "
+ "{template .main kind=\"js\"}",
join("{namespace ns}\n\n", "{template .main kind=\"js\"}\n", "var x='\n", "{/template}"));
}
@Test
public void testStrictUriMustNotBeEmpty() {
assertRewriteFails(
"In file no-path:3:1, template ns.main: "
+ "A strict block of kind=\"uri\" cannot end in context (Context URI START NORMAL). "
+ "Likely cause is an unterminated or empty URI: "
+ "{template .main autoescape=\"strict\" kind=\"uri\"}",
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"strict\" kind=\"uri\"}\n",
"{/template}"));
}
@Test
public void testContextualCanCallStrictModeUri() {
// This ensures that a contextual template ns.can use a strict URI -- specifically testing that
// the contextual call site matching doesn't do an exact match on context (which would be
// sensitive to whether single quotes or double quotes are used) but uses the logic in
// Context.isValidStartContextForContentKindLoose().
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"<a href=\"{call .bar data=\"all\" /}\">Test</a>",
"\n{/template}\n\n",
"{template .bar autoescape=\"strict\" kind=\"uri\"}\n",
" {@param x: ?}\n",
"http://www.google.com/search?q={$x |escapeUri}",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"deprecated-contextual\"}\n",
"<a href=\"{call .bar data=\"all\" /}\">Test</a>",
"\n{/template}\n\n",
"{template .bar autoescape=\"strict\" kind=\"uri\"}\n",
" {@param x: ?}\n",
"http://www.google.com/search?q={$x}",
"\n{/template}"));
}
@Test
public void testStrictAttributes() {
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"onclick={$x |escapeJsValue |escapeHtmlAttributeNospace} ",
"style='{$y |filterCssValue |escapeHtmlAttribute}' ",
"checked ",
"foo=\"bar\" ",
"title='{$z |escapeHtmlAttribute}'",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
" {@param x: ?}\n",
" {@param y: ?}\n",
" {@param z: ?}\n",
"onclick={$x} ",
"style='{$y}' ",
"checked ",
"foo=\"bar\" ",
"title='{$z}'",
"\n{/template}"));
}
@Test
public void testStrictAttributesMustNotEndInUnquotedAttributeValue() {
// Ensure that any final attribute-value pair is quoted -- otherwise, if the use site of the
// value forgets to add spaces, the next attribute will be swallowed.
assertRewriteFails(
"In file no-path:3:1, template ns.foo: "
+ "A strict block of kind=\"attributes\" cannot end in context "
+ "(Context JS SCRIPT SPACE_OR_TAG_END DIV_OP). "
+ "Likely cause is an unterminated attribute value, or ending with an unquoted "
+ "attribute: {template .foo autoescape=\"strict\" kind=\"attributes\"}",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
" {@param x: ?}\n",
"onclick={$x}",
"\n{/template}"));
assertRewriteFails(
"In file no-path:3:1, template ns.foo: "
+ "A strict block of kind=\"attributes\" cannot end in context "
+ "(Context HTML_NORMAL_ATTR_VALUE PLAIN_TEXT SPACE_OR_TAG_END). "
+ "Likely cause is an unterminated attribute value, or ending with an unquoted "
+ "attribute: {template .foo autoescape=\"strict\" kind=\"attributes\"}",
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
" {@param x: ?}\n",
"title={$x}",
"\n{/template}"));
}
@Test
public void testStrictAttributesCanEndInValuelessAttribute() {
// Allow ending in a valueless attribute like "checked". Unfortunately a sloppy user might end
// up having this collide with another attribute name.
// TODO: In the future, we might automatically add a space to the end of strict attributes.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
"foo=bar checked",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\" kind=\"attributes\"}\n",
"foo=bar checked",
"\n{/template}"));
}
@Test
public void testStrictModeJavascriptRegexHandling() {
// NOTE: This ensures that the call site is treated as a dynamic value, such that it switches
// from "before regexp" context to "before division" context. Note this isn't foolproof (such
// as when the expression leads to a full statement) but is generally going to be correct more
// often.
assertContextualRewriting(
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<script>",
"{call .bar /}/{$x |escapeJsValue}+/{$x |escapeJsRegex}/g",
"</script>",
"\n{/template}"),
join(
"{namespace ns}\n\n",
"{template .foo autoescape=\"strict\"}\n",
" {@param x: ?}\n",
"<script>",
"{call .bar /}/{$x}+/{$x}/g",
"</script>",
"\n{/template}"));
}
@Test
public void testStrictModeEscapesCallSites() {
String source =
"{namespace ns}\n\n"
+ "{template .main autoescape=\"strict\"}\n"
+ "{call .htmltemplate /}"
+ "<script>var x={call .htmltemplate /};</script>\n"
+ "<script>var x={call .jstemplate /};</script>\n"
+ "{call .externtemplate /}"
+ "\n{/template}\n\n"
+ "{template .htmltemplate autoescape=\"strict\"}\n"
+ "Hello World"
+ "\n{/template}\n\n"
+ "{template .jstemplate autoescape=\"strict\" kind=\"js\"}\n"
+ "foo()"
+ "\n{/template}";
ErrorReporter boom = ExplodingErrorReporter.get();
ParseResult parseResult =
SoyFileSetParserBuilder.forFileContents(source).errorReporter(boom).parse();
SoyFileSetNode soyTree = parseResult.fileSet();
new ContextualAutoescaper(SOY_PRINT_DIRECTIVES).rewrite(soyTree, parseResult.registry(), boom);
TemplateNode mainTemplate = soyTree.getChild(0).getChild(0);
assertWithMessage("Sanity check").that(mainTemplate.getTemplateName()).isEqualTo("ns.main");
final List<CallNode> callNodes = SoyTreeUtils.getAllNodesOfType(mainTemplate, CallNode.class);
assertThat(callNodes).hasSize(4);
assertWithMessage("HTML->HTML escaping should be pruned")
.that(callNodes.get(0).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of());
assertWithMessage("JS -> HTML call should be escaped")
.that(callNodes.get(1).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of("|escapeJsValue"));
assertWithMessage("JS -> JS pruned")
.that(callNodes.get(2).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of());
assertWithMessage("HTML -> extern call should be escaped")
.that(callNodes.get(3).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of("|escapeHtml"));
}
@Test
public void testStrictModeOptimizesDelegates() {
String source =
"{namespace ns}\n\n"
+ "{template .main autoescape=\"strict\"}\n"
+ "{delcall ns.delegateHtml /}"
+ "{delcall ns.delegateText /}"
+ "\n{/template}\n\n"
+ "/** A delegate returning HTML. */\n"
+ "{deltemplate ns.delegateHtml autoescape=\"strict\"}\n"
+ "Hello World"
+ "\n{/deltemplate}\n\n"
+ "/** A delegate returning JS. */\n"
+ "{deltemplate ns.delegateText autoescape=\"strict\" kind=\"text\"}\n"
+ "Hello World"
+ "\n{/deltemplate}";
ErrorReporter boom = ExplodingErrorReporter.get();
ParseResult parseResult =
SoyFileSetParserBuilder.forFileContents(source).errorReporter(boom).parse();
SoyFileSetNode soyTree = parseResult.fileSet();
new ContextualAutoescaper(SOY_PRINT_DIRECTIVES).rewrite(soyTree, parseResult.registry(), boom);
TemplateNode mainTemplate = soyTree.getChild(0).getChild(0);
assertWithMessage("Sanity check").that(mainTemplate.getTemplateName()).isEqualTo("ns.main");
final List<CallNode> callNodes = SoyTreeUtils.getAllNodesOfType(mainTemplate, CallNode.class);
assertThat(callNodes).hasSize(2);
assertWithMessage("We're compiling a complete set; we can optimize based on usages.")
.that(callNodes.get(0).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of());
assertWithMessage("HTML -> TEXT requires escaping")
.that(callNodes.get(1).getEscapingDirectiveNames())
.isEqualTo(ImmutableList.of("|escapeHtml"));
}
private static String getForbiddenMsgError(String path, String template, String context) {
return "In file "
+ path
+ ", template ns."
+ template
+ ": "
+ "Messages are not supported in this context, because it would mean asking translators to "
+ "write source code; if this is desired, try factoring the message into a {let} block: "
+ "(Context "
+ context
+ ")";
}
@Test
public void testMsgForbiddenUriStartContext() {
assertRewriteFails(
getForbiddenMsgError("no-path:4:12", "main", "URI NORMAL URI DOUBLE_QUOTE START NORMAL"),
join(
"{namespace ns}\n\n",
"{template .main}\n",
" <a href=\"{msg desc=\"foo\"}message{/msg}\">test</a>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:12", "main", "URI NORMAL URI DOUBLE_QUOTE START NORMAL"),
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" <a href=\"{msg desc=\"foo\"}message{/msg}\">test</a>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:3", "main", "URI START NORMAL"),
join(
"{namespace ns}\n\n",
"{template .main kind=\"uri\"}\n",
" {msg desc=\"foo\"}message{/msg}\n",
"{/template}"));
}
@Test
public void testMsgForbiddenJsContext() {
assertRewriteFails(
getForbiddenMsgError("no-path:4:11", "main", "JS REGEX"),
join(
"{namespace ns}\n\n",
"{template .main}\n",
" <script>{msg desc=\"foo\"}message{/msg}</script>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:11", "main", "JS REGEX"),
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" <script>{msg desc=\"foo\"}message{/msg}</script>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:3", "main", "JS REGEX"),
join(
"{namespace ns}\n\n",
"{template .main kind=\"js\"}\n",
" {msg desc=\"foo\"}message{/msg}\n",
"{/template}"));
}
@Test
public void testMsgForbiddenHtmlContexts() {
assertRewriteFails(
getForbiddenMsgError("no-path:4:8", "main", "HTML_TAG NORMAL"),
join(
"{namespace ns}\n\n",
"{template .main}\n",
" <div {msg desc=\"foo\"}attributes{/msg}>Test</div>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:4", "main", "HTML_BEFORE_OPEN_TAG_NAME"),
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" <{msg desc=\"foo\"}tagname{/msg}>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:5", "main", "HTML_BEFORE_CLOSE_TAG_NAME"),
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" </{msg desc=\"foo\"}tagname{/msg}>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:3", "main", "HTML_TAG"),
join(
"{namespace ns}\n\n",
"{template .main kind=\"attributes\"}\n",
" {msg desc=\"foo\"}message{/msg}\n",
"{/template}"));
}
@Test
public void testMsgForbiddenCssContext() {
assertRewriteFails(
getForbiddenMsgError("no-path:4:10", "main", "CSS"),
join(
"{namespace ns}\n\n",
"{template .main}\n",
" <style>{msg desc=\"foo\"}message{/msg}</style>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:10", "main", "CSS"),
join(
"{namespace ns}\n\n",
"{template .main autoescape=\"deprecated-contextual\"}\n",
" <style>{msg desc=\"foo\"}message{/msg}</style>\n",
"{/template}"));
assertRewriteFails(
getForbiddenMsgError("no-path:4:3", "main", "CSS"),
join(
"{namespace ns}\n\n",
"{template .main kind=\"css\"}\n",
" {msg desc=\"foo\"}message{/msg}\n",
"{/template}"));
}
// TODO: Tests for dynamic attributes: <a on{$name}="...">,
// <div data-{$name}={$value}>
private static String join(String... lines) {
return Joiner.on("").join(lines);
}
/**
* Returns the contextually rewritten source.
*
* <p>The Soy tree may have multiple files, but only the source code for the first is returned.
*/
private static String rewrittenSource(SoyFileSetNode soyTree) {
FormattingErrorReporter reporter = new FormattingErrorReporter();
List<TemplateNode> tmpls =
new ContextualAutoescaper(SOY_PRINT_DIRECTIVES)
.rewrite(soyTree, new TemplateRegistry(soyTree, reporter), reporter);
if (!reporter.getErrorMessages().isEmpty()) {
String message = reporter.getErrorMessages().get(0);
if (message.startsWith(ContextualAutoescaper.AUTOESCAPE_ERROR_PREFIX)) {
// Grab the part after the prefix (and the "- " used for indentation).
message = message.substring(ContextualAutoescaper.AUTOESCAPE_ERROR_PREFIX.length() + 2);
// Re-throw as an exception, so that tests are easier to write. I considered having the
// tests explicitly check the error messages; however, there's a substantial risk that some
// positive test might forget to check the error messages, and it leaves all callers of
// this with two things to check.
// TODO(gboyer): Once 100% of the contextual autoescaper's errors are migrated to the error
// reporter, we can stop throwing and simply add explicit checks in the cases.
throw SoyAutoescapeException.createWithoutMetaInfo(message);
} else {
throw new IllegalStateException("Unexpected error: " + message);
}
}
StringBuilder src = new StringBuilder();
src.append(soyTree.getChild(0).toSourceString());
for (TemplateNode tn : tmpls) {
src.append('\n').append(tn.toSourceString());
}
return src.toString();
}
private void assertContextualRewriting(String expectedOutput, String... inputs)
throws SoyAutoescapeException {
ErrorReporter boom = ExplodingErrorReporter.get();
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(inputs)
.errorReporter(boom)
.allowUnboundGlobals(true)
.parse()
.fileSet();
String source = rewrittenSource(soyTree);
assertThat(source.trim()).isEqualTo(expectedOutput);
}
private void assertContextualRewritingNoop(String expectedOutput) throws SoyAutoescapeException {
assertContextualRewriting(expectedOutput, expectedOutput);
}
/**
* @param msg Message that should be reported to the template ns.author. Null means don't care.
*/
private static void assertRewriteFails(@Nullable String msg, String... inputs) {
SoyFileSupplier[] soyFileSuppliers = new SoyFileSupplier[inputs.length];
for (int i = 0; i < inputs.length; ++i) {
soyFileSuppliers[i] =
SoyFileSupplier.Factory.create(
inputs[i], SoyFileKind.SRC, inputs.length == 1 ? "no-path" : "no-path-" + i);
}
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forSuppliers(soyFileSuppliers)
.declaredSyntaxVersion(SyntaxVersion.V2_0)
.parse()
.fileSet();
try {
rewrittenSource(soyTree);
} catch (SoyAutoescapeException ex) {
// Find the root cause; during contextualization, we re-wrap exceptions on the path to a
// template.
while (ex.getCause() instanceof SoyAutoescapeException) {
ex = (SoyAutoescapeException) ex.getCause();
}
if (msg != null && !msg.equals(ex.getMessage())) {
throw (ComparisonFailure) new ComparisonFailure("", msg, ex.getMessage()).initCause(ex);
}
return;
}
fail("Expected failure but was " + soyTree.getChild(0).toSourceString());
}
static final class FakeBidiSpanWrapDirective
implements SoyPrintDirective, SanitizedContentOperator {
@Override
public String getName() {
return "|bidiSpanWrap";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(0);
}
@Override
public boolean shouldCancelAutoescape() {
return false;
}
@Override
@Nonnull
public SanitizedContent.ContentKind getContentKind() {
return SanitizedContent.ContentKind.HTML;
}
}
}