Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically escape all XML special chars in attribute values when l… #57

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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