Skip to content

Commit

Permalink
Merge pull request #57 from EvoEsports/feature/escape-xml-special-chars
Browse files Browse the repository at this point in the history
Automatically escape all XML special chars in attribute values when l…
  • Loading branch information
araszka authored Feb 18, 2024
2 parents a04ef26 + cefd4ea commit b24faf4
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 94 deletions.
7 changes: 4 additions & 3 deletions src/ManiaTemplates/Components/MtComponent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Security;
using System.Xml;
using ManiaTemplates.Exceptions;
using ManiaTemplates.Lib;
Expand Down Expand Up @@ -88,7 +89,7 @@ public static MtComponent FromTemplate(ManiaTemplateEngine engine, string templa
private static XmlNode FindComponentNode(string templateContent)
{
var doc = new XmlDocument();
doc.LoadXml(Helper.EscapePropertyTypes(templateContent));
doc.LoadXml(MtSpecialCharEscaper.EscapeXmlSpecialCharsInAttributes(templateContent));

foreach (XmlNode node in doc.ChildNodes)
{
Expand Down Expand Up @@ -184,7 +185,7 @@ private static MtComponentProperty ParseComponentProperty(XmlNode node)
break;

case "type":
type = Helper.ReverseEscapeXmlAttributeString(attribute.Value);
type = attribute.Value;
break;

case "default":
Expand Down Expand Up @@ -247,7 +248,7 @@ private static HashSet<string> GetSlotNamesInTemplate(XmlNode node)
{
throw new DuplicateSlotException($"""A slot with the name "{slotName}" already exists.""");
}

slotNames.Add(slotName);
}
}
Expand Down
85 changes: 2 additions & 83 deletions src/ManiaTemplates/Lib/Helper.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using ManiaTemplates.Components;

namespace ManiaTemplates.Lib;

public abstract partial class Helper
public abstract class Helper
{
/// <summary>
/// Gets the embedded resources contents of the given assembly.
Expand All @@ -24,38 +18,6 @@ public static async Task<string> GetEmbeddedResourceContentAsync(string path, As
return await new StreamReader(stream).ReadToEndAsync();
}

/// <summary>
/// Creates a hash from the given string with a fixed length.
/// </summary>
internal static string Hash(string input)
{
return input.GetHashCode().ToString().Replace('-', 'N');
}

/// <summary>
/// Creates random alpha-numeric string with given length.
/// </summary>
public static string RandomString(int length = 16)
{
var random = new Random();
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}

/// <summary>
/// Determines whether a XML-node uses one of the given components.
/// </summary>
internal static bool UsesComponents(XmlNode node, MtComponentMap mtComponents)
{
foreach (XmlNode child in node.ChildNodes)
{
return UsesComponents(child, mtComponents);
}

return mtComponents.ContainsKey(node.Name);
}

/// <summary>
/// Takes a XML-string and aligns all nodes properly.
/// </summary>
Expand All @@ -81,47 +43,4 @@ public static string PrettyXml(string? uglyXml = null)

return stringBuilder.ToString();
}

/// <summary>
/// Escape all type-Attributes on property-Nodes in the given XML.
/// </summary>
public static string EscapePropertyTypes(string inputXml)
{
var outputXml = inputXml;
var propertyMatcher = ComponentPropertyMatcher();
var match = propertyMatcher.Match(inputXml);

while (match.Success)
{
var unescapedAttribute = match.Groups[1].Value;
outputXml = outputXml.Replace(unescapedAttribute, EscapeXmlAttributeString(unescapedAttribute));

match = match.NextMatch();
}

return outputXml;
}

/// <summary>
/// Takes the value of a XML-attribute and escapes special chars, which would break the XML reader.
/// </summary>
private static string EscapeXmlAttributeString(string attributeValue)
{
return attributeValue.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("&", "&amp;");
}

/// <summary>
/// Takes the escaped value of a XML-attribute and converts it back into it's original form.
/// </summary>
public static string ReverseEscapeXmlAttributeString(string attributeValue)
{
return attributeValue.Replace("&lt;", "<")
.Replace("&gt;", ">")
.Replace("&amp;", "&");
}

[GeneratedRegex("<property.+type=[\"'](.+?)[\"'].+(?:\\s*\\/>|<\\/property>)")]
private static partial Regex ComponentPropertyMatcher();
}
87 changes: 87 additions & 0 deletions src/ManiaTemplates/Lib/MtSpecialCharEscaper.cs
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;", "§%lt%§" },
{ "&gt;", "§%gt%§" },
{ "&amp;", "§%amp%§" },
{ "&quot;", "§%quot%§" },
{ "&apos;", "§%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);
}
}
6 changes: 0 additions & 6 deletions src/ManiaTemplates/Lib/MtTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,6 @@ MtComponent rootComponent
//Create available arguments
var componentRenderArguments = new List<string>();

//Add local variables with aliases
// foreach (var (alias, originalName) in contextAliasMap.Aliases)
// {
// componentRenderArguments.Add(CreateMethodCallArgument(originalName, alias));
// }

//Attach attributes to render method call
foreach (var (attributeName, attributeValue) in attributeList)
{
Expand Down
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&amp;'/>", processedSingleQuotes);

var processedDoubleQuotes = MtSpecialCharEscaper.FindAndEscapeAttributes("<SomeNode attribute=\"test>\"/>", MtSpecialCharEscaper.XmlTagAttributeMatcherDoubleQuote);
Assert.Equal("<SomeNode attribute=\"test&gt;\"/>", 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);
}
}
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>
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 &amp;&amp; i == i" text="{{ i }}" data-cond='{{ i > i }}'/>
</template>
</component>
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
<label text="outer_{{ __index }}_{{ i }}"/>
</TestComponentWithLoop>
</template>
</component>
</component>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<template>
<Frame if="enabled" foreach="int i in numbers" x="{{ 10 * __index }}">
<Label if="i &lt; numbers.Count" foreach="int j in numbers.GetRange(0, i)" text="{{ i }}, {{ j }} at index {{ __index }}, {{ __index2 }}"/>
<Label if="i < numbers.Count" foreach="int j in numbers.GetRange(0, i)" text="{{ i }}, {{ j }} at index {{ __index }}, {{ __index2 }}"/>
</Frame>
<Frame>
<Frame>
Expand Down

0 comments on commit b24faf4

Please sign in to comment.