diff --git a/src/ManiaTemplates/Components/MtComponent.cs b/src/ManiaTemplates/Components/MtComponent.cs index 484415f..8f3bcb0 100644 --- a/src/ManiaTemplates/Components/MtComponent.cs +++ b/src/ManiaTemplates/Components/MtComponent.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Security; using System.Xml; using ManiaTemplates.Exceptions; using ManiaTemplates.Lib; @@ -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) { @@ -184,7 +185,7 @@ private static MtComponentProperty ParseComponentProperty(XmlNode node) break; case "type": - type = Helper.ReverseEscapeXmlAttributeString(attribute.Value); + type = attribute.Value; break; case "default": @@ -247,7 +248,7 @@ private static HashSet GetSlotNamesInTemplate(XmlNode node) { throw new DuplicateSlotException($"""A slot with the name "{slotName}" already exists."""); } - + slotNames.Add(slotName); } } diff --git a/src/ManiaTemplates/Lib/Helper.cs b/src/ManiaTemplates/Lib/Helper.cs index 0ef041b..f6f6b8c 100644 --- a/src/ManiaTemplates/Lib/Helper.cs +++ b/src/ManiaTemplates/Lib/Helper.cs @@ -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 { /// /// Gets the embedded resources contents of the given assembly. @@ -24,38 +18,6 @@ public static async Task GetEmbeddedResourceContentAsync(string path, As return await new StreamReader(stream).ReadToEndAsync(); } - /// - /// Creates a hash from the given string with a fixed length. - /// - internal static string Hash(string input) - { - return input.GetHashCode().ToString().Replace('-', 'N'); - } - - /// - /// Creates random alpha-numeric string with given length. - /// - 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()); - } - - /// - /// Determines whether a XML-node uses one of the given components. - /// - internal static bool UsesComponents(XmlNode node, MtComponentMap mtComponents) - { - foreach (XmlNode child in node.ChildNodes) - { - return UsesComponents(child, mtComponents); - } - - return mtComponents.ContainsKey(node.Name); - } - /// /// Takes a XML-string and aligns all nodes properly. /// @@ -81,47 +43,4 @@ public static string PrettyXml(string? uglyXml = null) return stringBuilder.ToString(); } - - /// - /// Escape all type-Attributes on property-Nodes in the given XML. - /// - 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; - } - - /// - /// Takes the value of a XML-attribute and escapes special chars, which would break the XML reader. - /// - private static string EscapeXmlAttributeString(string attributeValue) - { - return attributeValue.Replace("<", "<") - .Replace(">", ">") - .Replace("&", "&"); - } - - /// - /// Takes the escaped value of a XML-attribute and converts it back into it's original form. - /// - public static string ReverseEscapeXmlAttributeString(string attributeValue) - { - return attributeValue.Replace("<", "<") - .Replace(">", ">") - .Replace("&", "&"); - } - - [GeneratedRegex("|<\\/property>)")] - private static partial Regex ComponentPropertyMatcher(); } diff --git a/src/ManiaTemplates/Lib/MtSpecialCharEscaper.cs b/src/ManiaTemplates/Lib/MtSpecialCharEscaper.cs new file mode 100644 index 0000000..8f83df3 --- /dev/null +++ b/src/ManiaTemplates/Lib/MtSpecialCharEscaper.cs @@ -0,0 +1,87 @@ +using System.Security; +using System.Text.RegularExpressions; + +namespace ManiaTemplates.Lib; + +public abstract class MtSpecialCharEscaper +{ + private static Dictionary _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-]+='(.+?)'"); + + /// + /// Takes a XML string and escapes all special chars in node attributes. + /// + 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; + } + + /// + /// Takes the string of a matched XML tag and escapes the attribute values. + /// The second argument is a regex to either match ='' or ="" attributes. + /// + 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)); + } + + /// + /// Takes a string and a key/value map. + /// Replaces all found keys in the string with the value. + /// + public static string SubstituteStrings(string input, Dictionary map) + { + var output = input; + foreach (var (escapeSequence, substitute) in map) + { + output = output.Replace(escapeSequence, substitute); + } + + return output; + } + + /// + /// Switches keys with values in the given dictionary and returns a new one. + /// + public static Dictionary FlipMapping(Dictionary map) + { + return map.ToDictionary(x => x.Value, x => x.Key); + } +} \ No newline at end of file diff --git a/src/ManiaTemplates/Lib/MtTransformer.cs b/src/ManiaTemplates/Lib/MtTransformer.cs index 000ec43..5f3bf67 100644 --- a/src/ManiaTemplates/Lib/MtTransformer.cs +++ b/src/ManiaTemplates/Lib/MtTransformer.cs @@ -358,12 +358,6 @@ MtComponent rootComponent //Create available arguments var componentRenderArguments = new List(); - //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) { diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/SpecialCharEscaperTest.cs b/tests/ManiaTemplates.Tests/IntegrationTests/SpecialCharEscaperTest.cs new file mode 100644 index 0000000..62e11d2 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/SpecialCharEscaperTest.cs @@ -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 + { + { "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 + { + { "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("", MtSpecialCharEscaper.XmlTagAttributeMatcherSingleQuote); + Assert.Equal("", processedSingleQuotes); + + var processedDoubleQuotes = MtSpecialCharEscaper.FindAndEscapeAttributes("\"/>", MtSpecialCharEscaper.XmlTagAttributeMatcherDoubleQuote); + Assert.Equal("", 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); + } +} \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/expected/escape-test.xml b/tests/ManiaTemplates.Tests/IntegrationTests/expected/escape-test.xml new file mode 100644 index 0000000..ce8bcaf --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/expected/escape-test.xml @@ -0,0 +1,5 @@ + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/escape-test.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/escape-test.mt new file mode 100644 index 0000000..2c3b855 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/escape-test.mt @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/loop-test.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/loop-test.mt index 8773fa0..8b24126 100644 --- a/tests/ManiaTemplates.Tests/IntegrationTests/templates/loop-test.mt +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/loop-test.mt @@ -29,4 +29,4 @@