Skip to content

Commit

Permalink
Merge pull request #473 from tommysor/NoEscape_inconsistent_fix
Browse files Browse the repository at this point in the history
HtmlEncoding consistent with rules in handlebars.js
  • Loading branch information
oformaniuk committed Dec 22, 2021
2 parents 61c9580 + 2fee90c commit 02e1794
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 26 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,43 @@ public void HelperWithDotSeparatedName()
}
```

#### HtmlEncoder
Used to switch between the legacy Handlebars.Net and the canonical Handlebars rules (or a custom implementation).\
For Handlebars.Net 2.x.x `HtmlEncoderLegacy` is the default.

`HtmlEncoder`\
Implements the canonical Handlebars rules.

`HtmlEncoderLegacy`\
Will not encode:\
= (equals)\
` (backtick)\
' (single quote)

Will encode non-ascii characters `â`, `ß`, ...\
Into HTML entities (`<`, `â`, `ß`, ...).

##### Areas
- `Runtime`

##### Example
```c#
[Fact]
public void UseCanonicalHtmlEncodingRules()
{
var handlebars = Handlebars.Create();
handlebars.Configuration.TextEncoder = new HtmlEncoder();

var source = "{{Text}}";
var value = new { Text = "< â" };

var template = handlebars.Compile(source);
var actual = template(value);

Assert.Equal("&lt; â", actual);
}
```

## Performance

### Compilation
Expand Down
66 changes: 62 additions & 4 deletions source/Handlebars.Test/BasicIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class HandlebarsEnvGenerator : IEnumerable<object[]>
types.Add(typeof(Dictionary<long, object>));
types.Add(typeof(Dictionary<string, string>));
})),
Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.TextEncoder = new HtmlEncoder())),
};

public IEnumerator<object[]> GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator();
Expand All @@ -37,6 +38,13 @@ public class HandlebarsEnvGenerator : IEnumerable<object[]>

public class BasicIntegrationTests
{
private static string HtmlEncodeStringHelper(IHandlebars handlebars, string inputString)
{
using var stringWriter = new System.IO.StringWriter();
handlebars.Configuration.TextEncoder.Encode(inputString, stringWriter);
return stringWriter.ToString();
}

[Theory]
[ClassData(typeof(HandlebarsEnvGenerator))]
public void BasicEnumerableFormatter(IHandlebars handlebars)
Expand Down Expand Up @@ -98,7 +106,9 @@ public void BasicPathUnresolvedBindingFormatter(IHandlebars handlebars)
name = "Handlebars.Net"
};
var result = template(data);
Assert.Equal("Hello, ('foo' is undefined)!", result);

var expected = HtmlEncodeStringHelper(handlebars, "Hello, ('foo' is undefined)!");
Assert.Equal(expected, result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
Expand All @@ -114,7 +124,9 @@ public void PathUnresolvedBindingFormatter(IHandlebars handlebars)
name = "Handlebars.Net"
};
var result = template(data);
Assert.Equal("Hello, ('foo' is undefined)!", result);

var expected = HtmlEncodeStringHelper(handlebars, "Hello, ('foo' is undefined)!");
Assert.Equal(expected, result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
Expand Down Expand Up @@ -355,7 +367,9 @@ public void BasicPathRelativeDotBinding(IHandlebars handlebars)
nestedObject = "Relative dots, yay"
};
var result = template(data);
Assert.Equal("{ nestedObject = Relative dots, yay }", result);

var expected = HtmlEncodeStringHelper(handlebars, "{ nestedObject = Relative dots, yay }");
Assert.Equal(expected, result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
Expand Down Expand Up @@ -1986,7 +2000,51 @@ public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName)
Assert.Equal("42", actual);
}
}


[Theory]
[InlineData(false, "=", "&#x3D;")]
[InlineData(true, "=", "=")]
public void HtmlEncoderCompatibilityIntegration(bool useLegacyHandlebarsNetHtmlEncoding, string inputChar, string expected)
{
var template = "{{InputChar}}";
var value = new
{
InputChar = inputChar
};

var config = new HandlebarsConfiguration
{
TextEncoder = useLegacyHandlebarsNetHtmlEncoding ? (ITextEncoder)new HtmlEncoderLegacy() : new HtmlEncoder()
};
var actual = Handlebars.Create(config).Compile(template).Invoke(value);

Assert.Equal(expected, actual);
}

[Theory]
[InlineData(false, "=", "&#x3D;")]
[InlineData(true, "=", "=")]
public void HtmlEncoderCompatibilityIntegration_LateChangeConfig(bool useLegacyHandlebarsNetHtmlEncoding, string inputChar, string expected)
{
var template = "{{InputChar}}";
var value = new
{
InputChar = inputChar
};

var config = new HandlebarsConfiguration
{
TextEncoder = !useLegacyHandlebarsNetHtmlEncoding ? (ITextEncoder)new HtmlEncoderLegacy() : new HtmlEncoder()
};
var handlebars = Handlebars.Create(config);
handlebars.Configuration.TextEncoder = useLegacyHandlebarsNetHtmlEncoding ? (ITextEncoder)new HtmlEncoderLegacy() : new HtmlEncoder();
var compiledTemplate = handlebars.Compile(template);

var actual = compiledTemplate(value);

Assert.Equal(expected, actual);
}

private class StringHelperResolver : IHelperResolver
{
public bool TryResolveHelper(PathInfo name, Type targetType, out IHelperDescriptor<HelperOptions> helper)
Expand Down
90 changes: 90 additions & 0 deletions source/Handlebars.Test/HtmlEncoderLegacyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.IO;
using System.Text;
using Xunit;

namespace HandlebarsDotNet.Test
{
public class HtmlEncoderLegacyTests
{
private readonly HtmlEncoderLegacy _htmlEncoderLegacy;

public HtmlEncoderLegacyTests()
{
_htmlEncoderLegacy = new HtmlEncoderLegacy();
}

[Theory]
[InlineData("&", "&amp;")]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData("\"", "&quot;")]
[InlineData("â", "&#226;")]

// Don't escape.
[InlineData("'", "'")]
[InlineData("`", "`")]
[InlineData("=", "=")]
public void EscapeCorrectCharactersHandlebarsNetLegacyRules(string input, string expected)
{
using var writer = new StringWriter();

_htmlEncoderLegacy.Encode(input, writer);

Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData("", "")]
[InlineData(null, "")]
[InlineData(" ", " ")]
[InlineData("&", "&amp;")]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData(" > ", " &gt; ")]
[InlineData("", "&#65533;")]
[InlineData("�a", "&#65533;a")]
[InlineData("\"", "&quot;")]
[InlineData("&a&", "&amp;a&amp;")]
[InlineData("a&a", "a&amp;a")]
public void EncodeTestHandlebarsNetLegacyRules(string input, string expected)
{
// Arrange
using var writer = new StringWriter();

// Act
_htmlEncoderLegacy.Encode(input, writer);

// Assert
Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData(null, "")]
[InlineData("", "")]
[InlineData("a", "a")]
[InlineData("<", "&lt;")]
public void EncodeStringBuilderOverload(string input, string expected)
{
using var writer = new StringWriter();

var inputStringBuilder = input == null ? null : new StringBuilder(input);

_htmlEncoderLegacy.Encode(inputStringBuilder, writer);

Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData(null, "")]
[InlineData("a", "a")]
[InlineData("<", "&lt;")]
public void EncodeCharEnumeratorOverload(string input, string expected)
{
using var writer = new StringWriter();

_htmlEncoderLegacy.Encode(input?.GetEnumerator(), writer);

Assert.Equal(expected, writer.ToString());
}
}
}
65 changes: 60 additions & 5 deletions source/Handlebars.Test/HtmlEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,89 @@
using System.IO;
using System.Text;
using Xunit;

namespace HandlebarsDotNet.Test
{
public class HtmlEncoderTests
{
private readonly HtmlEncoder _htmlEncoder;

public HtmlEncoderTests()
{
_htmlEncoder = new HtmlEncoder();
}

[Theory]
// Escape chars based on https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js
[InlineData("&", "&amp;")]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData("\"", "&quot;")]
[InlineData("'", "&#x27;")]
[InlineData("`", "&#x60;")]
[InlineData("=", "&#x3D;")]

// Don't escape.
[InlineData("â", "â")]
public void EscapeCorrectCharacters(string input, string expected)
{
using var writer = new StringWriter();

_htmlEncoder.Encode(input, writer);

Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData("", "")]
[InlineData(null, "")]
[InlineData(" ", " ")]
[InlineData("&", "&amp;")]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData(" > "," &gt; ")]
[InlineData("", "&#65533;")]
[InlineData("�a", "&#65533;a")]
[InlineData(" > ", " &gt; ")]
[InlineData("\"", "&quot;")]
[InlineData("&a&", "&amp;a&amp;")]
[InlineData("a&a", "a&amp;a")]
public void EncodeTest(string input, string expected)
{
// Arrange
var htmlEncoder = new HtmlEncoder();
using var writer = new StringWriter();

// Act
htmlEncoder.Encode(input, writer);
_htmlEncoder.Encode(input, writer);

// Assert
Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData(null, "")]
[InlineData("", "")]
[InlineData("a", "a")]
[InlineData("<", "&lt;")]
public void EncodeStringBuilderOverload(string input, string expected)
{
using var writer = new StringWriter();

var inputStringBuilder = input == null ? null : new StringBuilder(input);

_htmlEncoder.Encode(inputStringBuilder, writer);

Assert.Equal(expected, writer.ToString());
}

[Theory]
[InlineData(null, "")]
[InlineData("a", "a")]
[InlineData("<", "&lt;")]
public void EncodeCharEnumeratorOverload(string input, string expected)
{
using var writer = new StringWriter();

_htmlEncoder.Encode(input?.GetEnumerator(), writer);

Assert.Equal(expected, writer.ToString());
}
}
}
4 changes: 2 additions & 2 deletions source/Handlebars.Test/TripleStashTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ public void UnencodedEncodedUnencoded()
var data = new
{
a_bool = false,
dangerous_value = "<div>There's HTML here</div>"
dangerous_value = "<div>There is HTML here</div>"
};

var result = template(data);
Assert.Equal("<div>There's HTML here</div>...&lt;div&gt;There's HTML here&lt;/div&gt;...<div>There's HTML here</div>!", result);
Assert.Equal("<div>There is HTML here</div>...&lt;div&gt;There is HTML here&lt;/div&gt;...<div>There is HTML here</div>!", result);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion source/Handlebars/Configuration/HandlebarsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public HandlebarsConfiguration()
RegisteredTemplates = new ObservableIndex<string, HandlebarsTemplate<TextWriter, object, object>, StringEqualityComparer>(stringEqualityComparer);

HelperResolvers = new ObservableList<IHelperResolver>();
TextEncoder = new HtmlEncoder();
TextEncoder = new HtmlEncoderLegacy();
FormatterProviders.Add(_undefinedFormatter);
}
}
Expand Down
Loading

0 comments on commit 02e1794

Please sign in to comment.