-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #57 from EvoEsports/feature/escape-xml-special-chars
Automatically escape all XML special chars in attribute values when l…
- Loading branch information
Showing
9 changed files
with
169 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
using System.Security; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace ManiaTemplates.Lib; | ||
|
||
public abstract class MtSpecialCharEscaper | ||
{ | ||
private static Dictionary<string, string> _map = new() | ||
{ | ||
{ "<", "§%lt%§" }, | ||
{ ">", "§%gt%§" }, | ||
{ "&", "§%amp%§" }, | ||
{ """, "§%quot%§" }, | ||
{ "'", "§%apos%§" } | ||
}; | ||
|
||
public static readonly Regex XmlTagFinderRegex = new("<[\\w-]+(?:\\s+[\\w-]+=[\"'].+?[\"'])+\\s*\\/?>"); | ||
public static readonly Regex XmlTagAttributeMatcherDoubleQuote = new("[\\w-]+=\"(.+?)\""); | ||
public static readonly Regex XmlTagAttributeMatcherSingleQuote = new("[\\w-]+='(.+?)'"); | ||
|
||
/// <summary> | ||
/// Takes a XML string and escapes all special chars in node attributes. | ||
/// </summary> | ||
public static string EscapeXmlSpecialCharsInAttributes(string inputXmlString) | ||
{ | ||
var outputXml = inputXmlString; | ||
var xmlTagMatcher = XmlTagFinderRegex; | ||
var tagMatch = xmlTagMatcher.Match(inputXmlString); | ||
|
||
while (tagMatch.Success) | ||
{ | ||
var unescapedXmlTag = tagMatch.Value; | ||
var escapedXmlTag = | ||
FindAndEscapeAttributes(unescapedXmlTag, XmlTagAttributeMatcherDoubleQuote); | ||
escapedXmlTag = FindAndEscapeAttributes(escapedXmlTag, XmlTagAttributeMatcherSingleQuote); | ||
|
||
outputXml = outputXml.Replace(unescapedXmlTag, escapedXmlTag); | ||
tagMatch = tagMatch.NextMatch(); | ||
} | ||
|
||
return outputXml; | ||
} | ||
|
||
/// <summary> | ||
/// Takes the string of a matched XML tag and escapes the attribute values. | ||
/// The second argument is a regex to either match ='' or ="" attributes. | ||
/// </summary> | ||
public static string FindAndEscapeAttributes(string input, Regex attributeWithQuoteOrApostrophePattern) | ||
{ | ||
var outputXml = SubstituteStrings(input, _map); | ||
var attributeMatch = attributeWithQuoteOrApostrophePattern.Match(outputXml); | ||
|
||
while (attributeMatch.Success) | ||
{ | ||
var unescapedAttributeValue = attributeMatch.Groups[1].Value; | ||
var escapedAttributeValue = SecurityElement.Escape(unescapedAttributeValue); | ||
outputXml = outputXml.Replace(unescapedAttributeValue, escapedAttributeValue); | ||
|
||
attributeMatch = attributeMatch.NextMatch(); | ||
} | ||
|
||
return SubstituteStrings(outputXml, FlipMapping(_map)); | ||
} | ||
|
||
/// <summary> | ||
/// Takes a string and a key/value map. | ||
/// Replaces all found keys in the string with the value. | ||
/// </summary> | ||
public static string SubstituteStrings(string input, Dictionary<string, string> map) | ||
{ | ||
var output = input; | ||
foreach (var (escapeSequence, substitute) in map) | ||
{ | ||
output = output.Replace(escapeSequence, substitute); | ||
} | ||
|
||
return output; | ||
} | ||
|
||
/// <summary> | ||
/// Switches keys with values in the given dictionary and returns a new one. | ||
/// </summary> | ||
public static Dictionary<string, string> FlipMapping(Dictionary<string, string> map) | ||
{ | ||
return map.ToDictionary(x => x.Value, x => x.Key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
tests/ManiaTemplates.Tests/IntegrationTests/SpecialCharEscaperTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using ManiaTemplates.Lib; | ||
|
||
namespace ManiaTemplates.Tests.IntegrationTests; | ||
|
||
public class SpecialCharEscaperTest | ||
{ | ||
private readonly ManiaTemplateEngine _maniaTemplateEngine = new(); | ||
|
||
[Fact] | ||
public void Should_Flip_Mapping() | ||
{ | ||
var mapping = new Dictionary<string, string> | ||
{ | ||
{ "one", "test1" }, | ||
{ "two", "test2" }, | ||
}; | ||
|
||
var flipped = MtSpecialCharEscaper.FlipMapping(mapping); | ||
Assert.Equal("one", flipped["test1"]); | ||
Assert.Equal("two", flipped["test2"]); | ||
} | ||
|
||
[Fact] | ||
public void Should_Substitutes_Elements_In_String() | ||
{ | ||
var mapping = new Dictionary<string, string> | ||
{ | ||
{ "match1", "Unit" }, | ||
{ "match2", "test" }, | ||
}; | ||
|
||
var processed = MtSpecialCharEscaper.SubstituteStrings("Hello match1 this is a match2.", mapping); | ||
Assert.Equal("Hello Unit this is a test.", processed); | ||
} | ||
|
||
[Fact] | ||
public void Should_Find_And_Escape_Attributes_In_Xml_Node() | ||
{ | ||
var processedSingleQuotes = MtSpecialCharEscaper.FindAndEscapeAttributes("<SomeNode some-attribute='test&'/>", MtSpecialCharEscaper.XmlTagAttributeMatcherSingleQuote); | ||
Assert.Equal("<SomeNode some-attribute='test&'/>", processedSingleQuotes); | ||
|
||
var processedDoubleQuotes = MtSpecialCharEscaper.FindAndEscapeAttributes("<SomeNode attribute=\"test>\"/>", MtSpecialCharEscaper.XmlTagAttributeMatcherDoubleQuote); | ||
Assert.Equal("<SomeNode attribute=\"test>\"/>", processedDoubleQuotes); | ||
} | ||
|
||
[Fact] | ||
public async void Should_Escape_Special_Chars_In_Attributes() | ||
{ | ||
var escapeTestComponent = await File.ReadAllTextAsync("IntegrationTests/templates/escape-test.mt"); | ||
var expected = await File.ReadAllTextAsync("IntegrationTests/expected/escape-test.xml"); | ||
var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; | ||
|
||
_maniaTemplateEngine.AddTemplateFromString("EscapeTest", escapeTestComponent); | ||
|
||
var template = _maniaTemplateEngine.RenderAsync("EscapeTest", new | ||
{ | ||
data = Enumerable.Range(0, 4).ToList() | ||
}, assemblies).Result; | ||
Assert.Equal(expected, template, ignoreLineEndingDifferences: true); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
tests/ManiaTemplates.Tests/IntegrationTests/expected/escape-test.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<manialink version="3" id="MtEscapeTest" name="EvoSC#-MtEscapeTest"> | ||
<label text="2" data-cond="True" /> | ||
<label text="3" data-cond="True" /> | ||
<label text="0" data-cond="False" /> | ||
</manialink> |
8 changes: 8 additions & 0 deletions
8
tests/ManiaTemplates.Tests/IntegrationTests/templates/escape-test.mt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<component> | ||
<property type="List<int>" name="data"/> | ||
|
||
<template> | ||
<label foreach="int i in data" if="i > 1 && i < 4" text="{{ i }}" data-cond="{{ i >= 0 }}"/> | ||
<label foreach="int i in data" if="i < 1 && i == i" text="{{ i }}" data-cond='{{ i > i }}'/> | ||
</template> | ||
</component> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,4 +29,4 @@ | |
<label text="outer_{{ __index }}_{{ i }}"/> | ||
</TestComponentWithLoop> | ||
</template> | ||
</component> | ||
</component> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters