From 60b6506bf90f0d119c92c755a466e44a3bc87c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20S=C3=B8rbr=C3=A5ten?= Date: Mon, 20 Dec 2021 12:57:17 +0100 Subject: [PATCH 1/5] HtmlEncoder escape chars based on https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js --- .../Handlebars.Test/BasicIntegrationTests.cs | 52 ++++++++++- source/Handlebars.Test/HtmlEncoderTests.cs | 86 ++++++++++++++++++- .../Handlebars/Configuration/Compatibility.cs | 9 ++ .../Configuration/HandlebarsConfiguration.cs | 2 +- source/Handlebars/IO/HtmlEncoder.cs | 62 ++++++++++++- 5 files changed, 206 insertions(+), 5 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 31b2852f..be6cc0e4 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -1986,7 +1986,57 @@ public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) Assert.Equal("42", actual); } } - + + [Theory] + [InlineData(false, "=", "=")] + [InlineData(true, "=", "=")] + public void HtmlEncoderCompatibilityIntegration(bool useLegacyHandlebarsNetHtmlEncoding, string inputChar, string expected) + { + var template = "{{InputChar}}"; + var value = new + { + InputChar = inputChar + }; + + var config = new HandlebarsConfiguration + { + Compatibility = + { + UseLegacyHandlebarsNetHtmlEncoding = useLegacyHandlebarsNetHtmlEncoding + } + }; + var actual = Handlebars.Create(config).Compile(template).Invoke(value); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(false, "=", "=")] + [InlineData(true, "=", "=")] + public void HtmlEncoderCompatibilityIntegration_LateChangeConfig(bool useLegacyHandlebarsNetHtmlEncoding, string inputChar, string expected) + { + var template = "{{InputChar}}"; + var value = new + { + InputChar = inputChar + }; + + var config = new HandlebarsConfiguration + { + Compatibility = + { + UseLegacyHandlebarsNetHtmlEncoding = !useLegacyHandlebarsNetHtmlEncoding + } + }; + var handlebars = Handlebars.Create(config); + var compiledTemplate = handlebars.Compile(template); + + handlebars.Configuration.Compatibility.UseLegacyHandlebarsNetHtmlEncoding = useLegacyHandlebarsNetHtmlEncoding; + var actual = compiledTemplate(value); + + Assert.Equal(expected, actual); + } + private class StringHelperResolver : IHelperResolver { public bool TryResolveHelper(PathInfo name, Type targetType, out IHelperDescriptor helper) diff --git a/source/Handlebars.Test/HtmlEncoderTests.cs b/source/Handlebars.Test/HtmlEncoderTests.cs index 6a8022e3..ef427b01 100644 --- a/source/Handlebars.Test/HtmlEncoderTests.cs +++ b/source/Handlebars.Test/HtmlEncoderTests.cs @@ -5,6 +5,64 @@ namespace HandlebarsDotNet.Test { public class HtmlEncoderTests { + [Theory] + // Escape chars based on https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js + [InlineData("&", "&")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData("\"", """)] + [InlineData("'", "'")] + [InlineData("`", "`")] + [InlineData("=", "=")] + + // Don't escape. + [InlineData("â", "â")] + public void EscapeCorrectCharacters(string input, string expected) + { + var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = false }; + var htmlEncoder = new HtmlEncoder(compatibility); + using var writer = new StringWriter(); + + htmlEncoder.Encode(input, writer); + + Assert.Equal(expected, writer.ToString()); + } + + [Theory] + [InlineData("&", "&")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData("\"", """)] + [InlineData("â", "â")] + + // Don't escape. + [InlineData("'", "'")] + [InlineData("`", "`")] + [InlineData("=", "=")] + public void EscapeCorrectCharactersHandlebarsNetLegacyRules(string input, string expected) + { + var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; + var htmlEncoder = new HtmlEncoder(compatibility); + using var writer = new StringWriter(); + + htmlEncoder.Encode(input, writer); + + Assert.Equal(expected, writer.ToString()); + } + + [Fact] + public void EscapeCorrectCharacters_LateChangeConfig() + { + var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; + var htmlEncoder = new HtmlEncoder(compatibility); + using var writer = new StringWriter(); + + compatibility.UseLegacyHandlebarsNetHtmlEncoding = false; + htmlEncoder.Encode("â", writer); + + Assert.Equal("â", writer.ToString()); + } + [Theory] [InlineData("", "")] [InlineData(null, "")] @@ -18,10 +76,36 @@ public class HtmlEncoderTests [InlineData("\"", """)] [InlineData("&a&", "&a&")] [InlineData("a&a", "a&a")] + public void EncodeTestHandlebarsNetLegacyRules(string input, string expected) + { + // Arrange + var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; + var htmlEncoder = new HtmlEncoder(compatibility); + using var writer = new StringWriter(); + + // Act + htmlEncoder.Encode(input, writer); + + // Assert + Assert.Equal(expected, writer.ToString()); + } + + [Theory] + [InlineData("", "")] + [InlineData(null, "")] + [InlineData(" ", " ")] + [InlineData("&", "&")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData(" > ", " > ")] + [InlineData("\"", """)] + [InlineData("&a&", "&a&")] + [InlineData("a&a", "a&a")] public void EncodeTest(string input, string expected) { // Arrange - var htmlEncoder = new HtmlEncoder(); + var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = false }; + var htmlEncoder = new HtmlEncoder(compatibility); using var writer = new StringWriter(); // Act diff --git a/source/Handlebars/Configuration/Compatibility.cs b/source/Handlebars/Configuration/Compatibility.cs index 7ef8c8fb..2453aeab 100644 --- a/source/Handlebars/Configuration/Compatibility.cs +++ b/source/Handlebars/Configuration/Compatibility.cs @@ -16,5 +16,14 @@ public class Compatibility /// Such naming is not supported in Handlebarsjs and would break compatibility. /// public bool RelaxedHelperNaming { get; set; } = false; + + /// + /// If enables legacy encoding rules. + /// + /// This will encode non-ascii characters. + /// this will not encode '=', '`' or ''' (single quote). + /// + /// + public bool UseLegacyHandlebarsNetHtmlEncoding { get; set; } = true; } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs index a4325716..1fcd756e 100644 --- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -76,7 +76,7 @@ public HandlebarsConfiguration() RegisteredTemplates = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer); HelperResolvers = new ObservableList(); - TextEncoder = new HtmlEncoder(); + TextEncoder = new HtmlEncoder(Compatibility); FormatterProviders.Add(_undefinedFormatter); } } diff --git a/source/Handlebars/IO/HtmlEncoder.cs b/source/Handlebars/IO/HtmlEncoder.cs index 3d15b962..71c78a11 100644 --- a/source/Handlebars/IO/HtmlEncoder.cs +++ b/source/Handlebars/IO/HtmlEncoder.cs @@ -12,6 +12,13 @@ namespace HandlebarsDotNet /// public class HtmlEncoder : ITextEncoder { + private readonly Compatibility _compatibility; + + public HtmlEncoder(Compatibility compatibility) + { + _compatibility = compatibility; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Encode(StringBuilder text, TextWriter target) { @@ -35,8 +42,20 @@ public void Encode(T text, TextWriter target) where T : IEnumerator EncodeImpl(text, target); } - - private static void EncodeImpl(T text, TextWriter target) where T: IEnumerator + + private void EncodeImpl(T text, TextWriter target) where T : IEnumerator + { + if (_compatibility.UseLegacyHandlebarsNetHtmlEncoding) + { + EncodeImplLegacyHandlebarsNet(text, target); + } + else + { + EncodeImplHandlebarsJs(text, target); + } + } + + private static void EncodeImplLegacyHandlebarsNet(T text, TextWriter target) where T : IEnumerator { while (text.MoveNext()) { @@ -68,5 +87,44 @@ private static void EncodeImpl(T text, TextWriter target) where T: IEnumerato } } } + + private static void EncodeImplHandlebarsJs(T text, TextWriter target) where T : IEnumerator + { + /* + * Based on: https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js + * As of 2021-12-20 / commit https://github.com/handlebars-lang/handlebars.js/commit/3fb331ef40ee1a8308dd83b8e5adbcd798d0adc9 + */ + while (text.MoveNext()) + { + var value = text.Current; + switch (value) + { + case '&': + target.Write("&"); + break; + case '<': + target.Write("<"); + break; + case '>': + target.Write(">"); + break; + case '"': + target.Write("""); + break; + case '\'': + target.Write("'"); + break; + case '`': + target.Write("`"); + break; + case '=': + target.Write("="); + break; + default: + target.Write(value); + break; + } + } + } } } \ No newline at end of file From aa087ce09ebc4fa3400c1237ef31b88e877919db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20S=C3=B8rbr=C3=A5ten?= Date: Mon, 20 Dec 2021 14:50:01 +0100 Subject: [PATCH 2/5] Change tests that would fail if default for UseLegacyHandlebarsNetHtmlEncoding was set to false. --- .../Handlebars.Test/BasicIntegrationTests.cs | 19 ++++++++++++++++--- source/Handlebars.Test/TripleStashTests.cs | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index be6cc0e4..3302de6a 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -37,6 +37,13 @@ public class HandlebarsEnvGenerator : IEnumerable 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) @@ -98,7 +105,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))] @@ -114,7 +123,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))] @@ -355,7 +366,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))] diff --git a/source/Handlebars.Test/TripleStashTests.cs b/source/Handlebars.Test/TripleStashTests.cs index 2c6882fa..489aaeca 100644 --- a/source/Handlebars.Test/TripleStashTests.cs +++ b/source/Handlebars.Test/TripleStashTests.cs @@ -94,11 +94,11 @@ public void UnencodedEncodedUnencoded() var data = new { a_bool = false, - dangerous_value = "
There's HTML here
" + dangerous_value = "
There is HTML here
" }; var result = template(data); - Assert.Equal("
There's HTML here
...<div>There's HTML here</div>...
There's HTML here
!", result); + Assert.Equal("
There is HTML here
...<div>There is HTML here</div>...
There is HTML here
!", result); } } } From 7e81feb5c3daf6b12677907fb64c7320df0be522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20S=C3=B8rbr=C3=A5ten?= Date: Tue, 21 Dec 2021 11:27:41 +0100 Subject: [PATCH 3/5] Replace bool feature toggle with alternative implementation of HtmlEncoder. --- .../Handlebars.Test/BasicIntegrationTests.cs | 14 +--- .../Handlebars.Test/HtmlEncoderLegacyTests.cs | 60 +++++++++++++++ source/Handlebars.Test/HtmlEncoderTests.cs | 77 +++---------------- .../Handlebars/Configuration/Compatibility.cs | 9 --- .../Configuration/HandlebarsConfiguration.cs | 2 +- source/Handlebars/IO/HtmlEncoder.cs | 54 +------------ source/Handlebars/IO/HtmlEncoderLegacy.cs | 76 ++++++++++++++++++ 7 files changed, 151 insertions(+), 141 deletions(-) create mode 100644 source/Handlebars.Test/HtmlEncoderLegacyTests.cs create mode 100644 source/Handlebars/IO/HtmlEncoderLegacy.cs diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 3302de6a..aa74b6b5 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -2013,10 +2013,7 @@ public void HtmlEncoderCompatibilityIntegration(bool useLegacyHandlebarsNetHtmlE var config = new HandlebarsConfiguration { - Compatibility = - { - UseLegacyHandlebarsNetHtmlEncoding = useLegacyHandlebarsNetHtmlEncoding - } + TextEncoder = useLegacyHandlebarsNetHtmlEncoding ? (ITextEncoder)new HtmlEncoderLegacy() : new HtmlEncoder() }; var actual = Handlebars.Create(config).Compile(template).Invoke(value); @@ -2036,15 +2033,12 @@ public void HtmlEncoderCompatibilityIntegration_LateChangeConfig(bool useLegacyH var config = new HandlebarsConfiguration { - Compatibility = - { - UseLegacyHandlebarsNetHtmlEncoding = !useLegacyHandlebarsNetHtmlEncoding - } + 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); - - handlebars.Configuration.Compatibility.UseLegacyHandlebarsNetHtmlEncoding = useLegacyHandlebarsNetHtmlEncoding; + var actual = compiledTemplate(value); Assert.Equal(expected, actual); diff --git a/source/Handlebars.Test/HtmlEncoderLegacyTests.cs b/source/Handlebars.Test/HtmlEncoderLegacyTests.cs new file mode 100644 index 00000000..b2d52225 --- /dev/null +++ b/source/Handlebars.Test/HtmlEncoderLegacyTests.cs @@ -0,0 +1,60 @@ +using System.IO; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class HtmlEncoderLegacyTests + { + private readonly HtmlEncoderLegacy _htmlEncoderLegacy; + + public HtmlEncoderLegacyTests() + { + _htmlEncoderLegacy = new HtmlEncoderLegacy(); + } + + [Theory] + [InlineData("&", "&")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData("\"", """)] + [InlineData("â", "â")] + + // 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("&", "&")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData(" > ", " > ")] + [InlineData("�", "�")] + [InlineData("�a", "�a")] + [InlineData("\"", """)] + [InlineData("&a&", "&a&")] + [InlineData("a&a", "a&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()); + } + } +} diff --git a/source/Handlebars.Test/HtmlEncoderTests.cs b/source/Handlebars.Test/HtmlEncoderTests.cs index ef427b01..7e6a1bd6 100644 --- a/source/Handlebars.Test/HtmlEncoderTests.cs +++ b/source/Handlebars.Test/HtmlEncoderTests.cs @@ -5,6 +5,13 @@ 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("&", "&")] @@ -19,74 +26,10 @@ public class HtmlEncoderTests [InlineData("â", "â")] public void EscapeCorrectCharacters(string input, string expected) { - var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = false }; - var htmlEncoder = new HtmlEncoder(compatibility); - using var writer = new StringWriter(); - - htmlEncoder.Encode(input, writer); - - Assert.Equal(expected, writer.ToString()); - } - - [Theory] - [InlineData("&", "&")] - [InlineData("<", "<")] - [InlineData(">", ">")] - [InlineData("\"", """)] - [InlineData("â", "â")] - - // Don't escape. - [InlineData("'", "'")] - [InlineData("`", "`")] - [InlineData("=", "=")] - public void EscapeCorrectCharactersHandlebarsNetLegacyRules(string input, string expected) - { - var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; - var htmlEncoder = new HtmlEncoder(compatibility); - using var writer = new StringWriter(); - - htmlEncoder.Encode(input, writer); - - Assert.Equal(expected, writer.ToString()); - } - - [Fact] - public void EscapeCorrectCharacters_LateChangeConfig() - { - var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; - var htmlEncoder = new HtmlEncoder(compatibility); - using var writer = new StringWriter(); - - compatibility.UseLegacyHandlebarsNetHtmlEncoding = false; - htmlEncoder.Encode("â", writer); - - Assert.Equal("â", writer.ToString()); - } - - [Theory] - [InlineData("", "")] - [InlineData(null, "")] - [InlineData(" ", " ")] - [InlineData("&", "&")] - [InlineData("<", "<")] - [InlineData(">", ">")] - [InlineData(" > "," > ")] - [InlineData("�", "�")] - [InlineData("�a", "�a")] - [InlineData("\"", """)] - [InlineData("&a&", "&a&")] - [InlineData("a&a", "a&a")] - public void EncodeTestHandlebarsNetLegacyRules(string input, string expected) - { - // Arrange - var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = true }; - var htmlEncoder = new HtmlEncoder(compatibility); using var writer = new StringWriter(); - // Act - htmlEncoder.Encode(input, writer); + _htmlEncoder.Encode(input, writer); - // Assert Assert.Equal(expected, writer.ToString()); } @@ -104,12 +47,10 @@ public void EncodeTestHandlebarsNetLegacyRules(string input, string expected) public void EncodeTest(string input, string expected) { // Arrange - var compatibility = new Compatibility { UseLegacyHandlebarsNetHtmlEncoding = false }; - var htmlEncoder = new HtmlEncoder(compatibility); using var writer = new StringWriter(); // Act - htmlEncoder.Encode(input, writer); + _htmlEncoder.Encode(input, writer); // Assert Assert.Equal(expected, writer.ToString()); diff --git a/source/Handlebars/Configuration/Compatibility.cs b/source/Handlebars/Configuration/Compatibility.cs index 2453aeab..7ef8c8fb 100644 --- a/source/Handlebars/Configuration/Compatibility.cs +++ b/source/Handlebars/Configuration/Compatibility.cs @@ -16,14 +16,5 @@ public class Compatibility /// Such naming is not supported in Handlebarsjs and would break compatibility. /// public bool RelaxedHelperNaming { get; set; } = false; - - /// - /// If enables legacy encoding rules. - /// - /// This will encode non-ascii characters. - /// this will not encode '=', '`' or ''' (single quote). - /// - /// - public bool UseLegacyHandlebarsNetHtmlEncoding { get; set; } = true; } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs index 1fcd756e..99b3062a 100644 --- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -76,7 +76,7 @@ public HandlebarsConfiguration() RegisteredTemplates = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer); HelperResolvers = new ObservableList(); - TextEncoder = new HtmlEncoder(Compatibility); + TextEncoder = new HtmlEncoderLegacy(); FormatterProviders.Add(_undefinedFormatter); } } diff --git a/source/Handlebars/IO/HtmlEncoder.cs b/source/Handlebars/IO/HtmlEncoder.cs index 71c78a11..7f442e41 100644 --- a/source/Handlebars/IO/HtmlEncoder.cs +++ b/source/Handlebars/IO/HtmlEncoder.cs @@ -12,13 +12,6 @@ namespace HandlebarsDotNet /// public class HtmlEncoder : ITextEncoder { - private readonly Compatibility _compatibility; - - public HtmlEncoder(Compatibility compatibility) - { - _compatibility = compatibility; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Encode(StringBuilder text, TextWriter target) { @@ -44,51 +37,6 @@ public void Encode(T text, TextWriter target) where T : IEnumerator } private void EncodeImpl(T text, TextWriter target) where T : IEnumerator - { - if (_compatibility.UseLegacyHandlebarsNetHtmlEncoding) - { - EncodeImplLegacyHandlebarsNet(text, target); - } - else - { - EncodeImplHandlebarsJs(text, target); - } - } - - private static void EncodeImplLegacyHandlebarsNet(T text, TextWriter target) where T : IEnumerator - { - while (text.MoveNext()) - { - var value = text.Current; - switch (value) - { - case '"': - target.Write("""); - break; - case '&': - target.Write("&"); - break; - case '<': - target.Write("<"); - break; - case '>': - target.Write(">"); - break; - - default: - if (value > 159) - { - target.Write("&#"); - target.Write((int)value); - target.Write(";"); - } - else target.Write(value); - break; - } - } - } - - private static void EncodeImplHandlebarsJs(T text, TextWriter target) where T : IEnumerator { /* * Based on: https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js @@ -127,4 +75,4 @@ private static void EncodeImplHandlebarsJs(T text, TextWriter target) where T } } } -} \ No newline at end of file +} diff --git a/source/Handlebars/IO/HtmlEncoderLegacy.cs b/source/Handlebars/IO/HtmlEncoderLegacy.cs new file mode 100644 index 00000000..fd3cc721 --- /dev/null +++ b/source/Handlebars/IO/HtmlEncoderLegacy.cs @@ -0,0 +1,76 @@ +using HandlebarsDotNet.StringUtils; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace HandlebarsDotNet +{ + /// + /// + /// Produces HTML safe output. + /// + /// This will encode non-ascii characters. + /// this will not encode '=', '`' or ''' (single quote). + /// + /// + public class HtmlEncoderLegacy : ITextEncoder + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encode(StringBuilder text, TextWriter target) + { + if (text == null || text.Length == 0) return; + + EncodeImpl(new StringBuilderEnumerator(text), target); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encode(string text, TextWriter target) + { + if (string.IsNullOrEmpty(text)) return; + + EncodeImpl(new StringEnumerator(text), target); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encode(T text, TextWriter target) where T : IEnumerator + { + if (text == null) return; + + EncodeImpl(text, target); + } + + private void EncodeImpl(T text, TextWriter target) where T : IEnumerator + { + while (text.MoveNext()) + { + var value = text.Current; + switch (value) + { + case '"': + target.Write("""); + break; + case '&': + target.Write("&"); + break; + case '<': + target.Write("<"); + break; + case '>': + target.Write(">"); + break; + + default: + if (value > 159) + { + target.Write("&#"); + target.Write((int)value); + target.Write(";"); + } + else target.Write(value); + break; + } + } + } + } +} From cc80d193a7fe0da8dc37f84337005c7731daca09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20S=C3=B8rbr=C3=A5ten?= Date: Wed, 22 Dec 2021 11:08:41 +0100 Subject: [PATCH 4/5] Make EncodeImpl static. Add unit tests for overloads. --- .../Handlebars.Test/HtmlEncoderLegacyTests.cs | 30 +++++++++++++++++++ source/Handlebars.Test/HtmlEncoderTests.cs | 30 +++++++++++++++++++ source/Handlebars/IO/HtmlEncoder.cs | 2 +- source/Handlebars/IO/HtmlEncoderLegacy.cs | 2 +- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/source/Handlebars.Test/HtmlEncoderLegacyTests.cs b/source/Handlebars.Test/HtmlEncoderLegacyTests.cs index b2d52225..8c747d0a 100644 --- a/source/Handlebars.Test/HtmlEncoderLegacyTests.cs +++ b/source/Handlebars.Test/HtmlEncoderLegacyTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using Xunit; namespace HandlebarsDotNet.Test @@ -56,5 +57,34 @@ public void EncodeTestHandlebarsNetLegacyRules(string input, string expected) // Assert Assert.Equal(expected, writer.ToString()); } + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("a", "a")] + [InlineData("<", "<")] + 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("<", "<")] + public void EncodeCharEnumeratorOverload(string input, string expected) + { + using var writer = new StringWriter(); + + _htmlEncoderLegacy.Encode(input?.GetEnumerator(), writer); + + Assert.Equal(expected, writer.ToString()); + } } } diff --git a/source/Handlebars.Test/HtmlEncoderTests.cs b/source/Handlebars.Test/HtmlEncoderTests.cs index 7e6a1bd6..d5d358df 100644 --- a/source/Handlebars.Test/HtmlEncoderTests.cs +++ b/source/Handlebars.Test/HtmlEncoderTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using Xunit; namespace HandlebarsDotNet.Test @@ -55,5 +56,34 @@ public void EncodeTest(string input, string expected) // Assert Assert.Equal(expected, writer.ToString()); } + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("a", "a")] + [InlineData("<", "<")] + 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("<", "<")] + public void EncodeCharEnumeratorOverload(string input, string expected) + { + using var writer = new StringWriter(); + + _htmlEncoder.Encode(input?.GetEnumerator(), writer); + + Assert.Equal(expected, writer.ToString()); + } } } diff --git a/source/Handlebars/IO/HtmlEncoder.cs b/source/Handlebars/IO/HtmlEncoder.cs index 7f442e41..495a547d 100644 --- a/source/Handlebars/IO/HtmlEncoder.cs +++ b/source/Handlebars/IO/HtmlEncoder.cs @@ -36,7 +36,7 @@ public void Encode(T text, TextWriter target) where T : IEnumerator EncodeImpl(text, target); } - private void EncodeImpl(T text, TextWriter target) where T : IEnumerator + private static void EncodeImpl(T text, TextWriter target) where T : IEnumerator { /* * Based on: https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js diff --git a/source/Handlebars/IO/HtmlEncoderLegacy.cs b/source/Handlebars/IO/HtmlEncoderLegacy.cs index fd3cc721..9e703e7f 100644 --- a/source/Handlebars/IO/HtmlEncoderLegacy.cs +++ b/source/Handlebars/IO/HtmlEncoderLegacy.cs @@ -40,7 +40,7 @@ public void Encode(T text, TextWriter target) where T : IEnumerator EncodeImpl(text, target); } - private void EncodeImpl(T text, TextWriter target) where T : IEnumerator + private static void EncodeImpl(T text, TextWriter target) where T : IEnumerator { while (text.MoveNext()) { From 2fee90c8fc4485f281cdcc74c3f3fe14e8eb7bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20S=C3=B8rbr=C3=A5ten?= Date: Wed, 22 Dec 2021 15:00:35 +0100 Subject: [PATCH 5/5] Add readme section. Run BasicIntegrationTests with new version of HtmlEncoder. --- README.md | 37 +++++++++++++++++++ .../Handlebars.Test/BasicIntegrationTests.cs | 1 + 2 files changed, 38 insertions(+) diff --git a/README.md b/README.md index b2b3e3ab..594cd2c1 100644 --- a/README.md +++ b/README.md @@ -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("< â", actual); +} +``` + ## Performance ### Compilation diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index aa74b6b5..00a4049f 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -28,6 +28,7 @@ public class HandlebarsEnvGenerator : IEnumerable types.Add(typeof(Dictionary)); types.Add(typeof(Dictionary)); })), + Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.TextEncoder = new HtmlEncoder())), }; public IEnumerator GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator();