From 8de579083c66ce94de0883e15a3f5347dd5587d3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 14 Feb 2013 16:23:56 -0100 Subject: [PATCH] U4-1611 - fix xpath special chars escaping issues in published content store --- src/Umbraco.Core/Umbraco.Core.csproj | 5 + src/Umbraco.Core/Xml/DynamicContext.cs | 312 ++++++++++++++++++ src/Umbraco.Core/Xml/XPathVariable.cs | 16 + src/Umbraco.Core/Xml/XmlNamespaces.cs | 41 +++ src/Umbraco.Core/Xml/XmlNodeExtensions.cs | 51 +++ src/Umbraco.Core/Xml/XmlNodeListFactory.cs | 178 ++++++++++ .../DefaultPublishedContentStore.cs | 35 +- 7 files changed, 631 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Core/Xml/DynamicContext.cs create mode 100644 src/Umbraco.Core/Xml/XPathVariable.cs create mode 100644 src/Umbraco.Core/Xml/XmlNamespaces.cs create mode 100644 src/Umbraco.Core/Xml/XmlNodeExtensions.cs create mode 100644 src/Umbraco.Core/Xml/XmlNodeListFactory.cs diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b183015b23..b75df3d16e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -724,6 +724,11 @@ + + + + + diff --git a/src/Umbraco.Core/Xml/DynamicContext.cs b/src/Umbraco.Core/Xml/DynamicContext.cs new file mode 100644 index 0000000000..858d1cd663 --- /dev/null +++ b/src/Umbraco.Core/Xml/DynamicContext.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Xsl; +using System.Xml.XPath; + +// source: mvpxml.codeplex.com + +namespace Umbraco.Core.Xml +{ + /// + /// Provides the evaluation context for fast execution and custom + /// variables resolution. + /// + /// + /// This class is responsible for resolving variables during dynamic expression execution. + /// Discussed in http://weblogs.asp.net/cazzu/archive/2003/10/07/30888.aspx + /// Author: Daniel Cazzulino, blog + /// + public class DynamicContext : XsltContext + { + #region Private vars + + readonly IDictionary _variables = + new Dictionary(); + + #endregion Private + + #region Constructors & Initialization + + /// + /// Initializes a new instance of the class. + /// + public DynamicContext() + : base(new NameTable()) + { + } + + /// + /// Initializes a new instance of the + /// class with the specified . + /// + /// The NameTable to use. + public DynamicContext(NameTable table) + : base(table) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A previously filled context with the namespaces to use. + public DynamicContext(XmlNamespaceManager context) + : this(context, new NameTable()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A previously filled context with the namespaces to use. + /// The NameTable to use. + public DynamicContext(XmlNamespaceManager context, NameTable table) + : base(table) + { + object xml = table.Add(XmlNamespaces.Xml); + object xmlns = table.Add(XmlNamespaces.XmlNs); + + if (context == null) return; + + foreach (string prefix in context) + { + var uri = context.LookupNamespace(prefix); + // Use fast object reference comparison to omit forbidden namespace declarations. + if (Equals(uri, xml) || Equals(uri, xmlns)) + continue; + if (uri == null) + continue; + base.AddNamespace(prefix, uri); + } + } + + #endregion Constructors & Initialization + + #region Common Overrides + + /// + /// Implementation equal to . + /// + public override int CompareDocument(string baseUri, string nextbaseUri) + { + return String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture); + } + + /// + /// Same as . + /// + public override string LookupNamespace(string prefix) + { + var key = NameTable.Get(prefix); + return key == null ? null : base.LookupNamespace(key); + } + + /// + /// Same as . + /// + public override string LookupPrefix(string uri) + { + var key = NameTable.Get(uri); + return key == null ? null : base.LookupPrefix(key); + } + + /// + /// Same as . + /// + public override bool PreserveWhitespace(XPathNavigator node) + { + return true; + } + + /// + /// Same as . + /// + public override bool Whitespace + { + get { return true; } + } + + #endregion Common Overrides + + #region Public Members + + /// + /// Shortcut method that compiles an expression using an empty navigator. + /// + /// The expression to compile + /// A compiled . + public static XPathExpression Compile(string xpath) + { + return new XmlDocument().CreateNavigator().Compile(xpath); + } + + #endregion Public Members + + #region Variable Handling Code + + /// + /// Adds the variable to the dynamic evaluation context. + /// + /// The name of the variable to add to the context. + /// The value of the variable to add to the context. + /// + /// Value type conversion for XPath evaluation is as follows: + /// + /// + /// CLR Type + /// XPath type + /// + /// + /// System.String + /// XPathResultType.String + /// + /// + /// System.Double (or types that can be converted to) + /// XPathResultType.Number + /// + /// + /// System.Boolean + /// XPathResultType.Boolean + /// + /// + /// System.Xml.XPath.XPathNavigator + /// XPathResultType.Navigator + /// + /// + /// System.Xml.XPath.XPathNodeIterator + /// XPathResultType.NodeSet + /// + /// + /// Others + /// XPathResultType.Any + /// + /// + /// See the topic "Compile, Select, Evaluate, and Matches with + /// XPath and XPathExpressions" in MSDN documentation for additional information. + /// + /// The is null. + public void AddVariable(string name, object value) + { + if (value == null) throw new ArgumentNullException("value"); + _variables[name] = new DynamicVariable(name, value); + } + + /// + /// See . Not used in our implementation. + /// + public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] argTypes) + { + return null; + } + + /// + /// Resolves the dynamic variables added to the context. See . + /// + public override IXsltContextVariable ResolveVariable(string prefix, string name) + { + IXsltContextVariable var; + _variables.TryGetValue(name, out var); + return var; + } + + #endregion Variable Handling Code + + #region Internal DynamicVariable class + + /// + /// Represents a variable during dynamic expression execution. + /// + internal class DynamicVariable : IXsltContextVariable + { + readonly string _name; + readonly object _value; + + #region Public Members + + public string Name { get { return _name; } } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the variable. + /// The value of the variable. + public DynamicVariable(string name, object value) + { + + _name = name; + _value = value; + + if (value is String) + _type = XPathResultType.String; + else if (value is bool) + _type = XPathResultType.Boolean; + else if (value is XPathNavigator) + _type = XPathResultType.Navigator; + else if (value is XPathNodeIterator) + _type = XPathResultType.NodeSet; + else + { + // Try to convert to double (native XPath numeric type) + if (value is double) + { + _type = XPathResultType.Number; + } + else + { + if (value is IConvertible) + { + try + { + _value = Convert.ToDouble(value); + // We suceeded, so it's a number. + _type = XPathResultType.Number; + } + catch (FormatException) + { + _type = XPathResultType.Any; + } + catch (OverflowException) + { + _type = XPathResultType.Any; + } + } + else + { + _type = XPathResultType.Any; + } + } + } + } + + #endregion Public Members + + #region IXsltContextVariable Implementation + + XPathResultType IXsltContextVariable.VariableType + { + get { return _type; } + } + + readonly XPathResultType _type; + + object IXsltContextVariable.Evaluate(XsltContext context) + { + return _value; + } + + bool IXsltContextVariable.IsLocal + { + get { return false; } + } + + bool IXsltContextVariable.IsParam + { + get { return false; } + } + + #endregion IXsltContextVariable Implementation + } + + #endregion Internal DynamicVariable class + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Xml/XPathVariable.cs b/src/Umbraco.Core/Xml/XPathVariable.cs new file mode 100644 index 0000000000..348a2ee3ea --- /dev/null +++ b/src/Umbraco.Core/Xml/XPathVariable.cs @@ -0,0 +1,16 @@ +// source: mvpxml.codeplex.com + +namespace Umbraco.Core.Xml +{ + internal class XPathVariable + { + public string Name { get; set; } + public string Value { get; set; } + + public XPathVariable(string name, string value) + { + Name = name; + Value = value; + } + } +} diff --git a/src/Umbraco.Core/Xml/XmlNamespaces.cs b/src/Umbraco.Core/Xml/XmlNamespaces.cs new file mode 100644 index 0000000000..aecbe9de87 --- /dev/null +++ b/src/Umbraco.Core/Xml/XmlNamespaces.cs @@ -0,0 +1,41 @@ +// source: mvpxml.codeplex.com + +namespace Umbraco.Core.Xml +{ + /// + /// Provides public constants for wellknown XML namespaces. + /// + /// Author: Daniel Cazzulino, blog + public static class XmlNamespaces + { + /// + /// The public XML 1.0 namespace. + /// + /// See http://www.w3.org/TR/2004/REC-xml-20040204/ + public const string Xml = "http://www.w3.org/XML/1998/namespace"; + + /// + /// Public Xml Namespaces specification namespace. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNs = "http://www.w3.org/2000/xmlns/"; + + /// + /// Public Xml Namespaces prefix. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNsPrefix = "xmlns"; + + /// + /// XML Schema instance namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance"; + + /// + /// XML 1.0 Schema namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsd = "http://www.w3.org/2001/XMLSchema"; + } +} diff --git a/src/Umbraco.Core/Xml/XmlNodeExtensions.cs b/src/Umbraco.Core/Xml/XmlNodeExtensions.cs new file mode 100644 index 0000000000..53a7ef08db --- /dev/null +++ b/src/Umbraco.Core/Xml/XmlNodeExtensions.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.XPath; + +// source: mvpxml.codeplex.com + +namespace Umbraco.Core.Xml +{ + internal static class XmlNodeExtensions + { + static XPathNodeIterator Select(string expression, XPathNavigator source, params XPathVariable[] variables) + { + var expr = source.Compile(expression); + var context = new DynamicContext(); + foreach (var variable in variables) + context.AddVariable(variable.Name, variable.Value); + expr.SetContext(context); + return source.Select(expr); + } + + public static XmlNodeList SelectNodes(this XmlNode source, string expression, IEnumerable variables) + { + var av = variables == null ? null : variables.ToArray(); + return SelectNodes(source, expression, av); + } + + public static XmlNodeList SelectNodes(this XmlNode source, string expression, params XPathVariable[] variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + return source.SelectNodes(expression); + + var iterator = Select(expression, source.CreateNavigator(), variables); + return XmlNodeListFactory.CreateNodeList(iterator); + } + + public static XmlNode SelectSingleNode(this XmlNode source, string expression, IEnumerable variables) + { + var av = variables == null ? null : variables.ToArray(); + return SelectSingleNode(source, expression, av); + } + + public static XmlNode SelectSingleNode(this XmlNode source, string expression, params XPathVariable[] variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + return source.SelectSingleNode(expression); + + return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); + } + } +} diff --git a/src/Umbraco.Core/Xml/XmlNodeListFactory.cs b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs new file mode 100644 index 0000000000..1bf945fa3f --- /dev/null +++ b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.XPath; + +// source: mvpxml.codeplex.com + +namespace Umbraco.Core.Xml +{ + class XmlNodeListFactory + { + private XmlNodeListFactory() { } + + #region Public members + + /// + /// Creates an instance of a that allows + /// enumerating elements in the iterator. + /// + /// The result of a previous node selection + /// through an query. + /// An initialized list ready to be enumerated. + /// The underlying XML store used to issue the query must be + /// an object inheriting , such as + /// . + public static XmlNodeList CreateNodeList(XPathNodeIterator iterator) + { + return new XmlNodeListIterator(iterator); + } + + #endregion Public members + + #region XmlNodeListIterator + + private class XmlNodeListIterator : XmlNodeList + { + readonly XPathNodeIterator _iterator; + readonly IList _nodes = new List(); + + public XmlNodeListIterator(XPathNodeIterator iterator) + { + _iterator = iterator.Clone(); + } + + public override System.Collections.IEnumerator GetEnumerator() + { + return new XmlNodeListEnumerator(this); + } + + public override XmlNode Item(int index) + { + + if (index >= _nodes.Count) + ReadTo(index); + // Compatible behavior with .NET + if (index >= _nodes.Count || index < 0) + return null; + return _nodes[index]; + } + + public override int Count + { + get + { + if (!_done) ReadToEnd(); + return _nodes.Count; + } + } + + + /// + /// Reads the entire iterator. + /// + private void ReadToEnd() + { + while (_iterator.MoveNext()) + { + var node = _iterator.Current as IHasXmlNode; + // Check IHasXmlNode interface. + if (node == null) + throw new ArgumentException("IHasXmlNode is missing."); + _nodes.Add(node.GetNode()); + } + _done = true; + } + + /// + /// Reads up to the specified index, or until the + /// iterator is consumed. + /// + private void ReadTo(int to) + { + while (_nodes.Count <= to) + { + if (_iterator.MoveNext()) + { + var node = _iterator.Current as IHasXmlNode; + // Check IHasXmlNode interface. + if (node == null) + throw new ArgumentException("IHasXmlNode is missing."); + _nodes.Add(node.GetNode()); + } + else + { + _done = true; + return; + } + } + } + + /// + /// Flags that the iterator has been consumed. + /// + private bool Done + { + get { return _done; } + } + + bool _done; + + /// + /// Current count of nodes in the iterator (read so far). + /// + private int CurrentPosition + { + get { return _nodes.Count; } + } + + #region XmlNodeListEnumerator + + private class XmlNodeListEnumerator : System.Collections.IEnumerator + { + readonly XmlNodeListIterator _iterator; + int _position = -1; + + public XmlNodeListEnumerator(XmlNodeListIterator iterator) + { + _iterator = iterator; + } + + #region IEnumerator Members + + void System.Collections.IEnumerator.Reset() + { + _position = -1; + } + + + bool System.Collections.IEnumerator.MoveNext() + { + _position++; + _iterator.ReadTo(_position); + + // If we reached the end and our index is still + // bigger, there're no more items. + if (_iterator.Done && _position >= _iterator.CurrentPosition) + return false; + + return true; + } + + object System.Collections.IEnumerator.Current + { + get + { + return _iterator[_position]; + } + } + + #endregion + } + + #endregion XmlNodeListEnumerator + } + + #endregion XmlNodeListIterator + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/DefaultPublishedContentStore.cs b/src/Umbraco.Web/DefaultPublishedContentStore.cs index bb54717c26..215461243c 100644 --- a/src/Umbraco.Web/DefaultPublishedContentStore.cs +++ b/src/Umbraco.Web/DefaultPublishedContentStore.cs @@ -4,6 +4,7 @@ using System.Text; using System.Xml; using System.Xml.Linq; using Umbraco.Core.Models; +using Umbraco.Core.Xml; using Umbraco.Web.Routing; using umbraco; using umbraco.NodeFactory; @@ -119,19 +120,20 @@ namespace Umbraco.Web int pos = route.IndexOf('/'); string path = pos == 0 ? route : route.Substring(pos); int startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos)); + IEnumerable vars; - var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode.Value); + var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode.Value, out vars); //check if we can find the node in our xml cache - var found = GetXml(umbracoContext).SelectSingleNode(xpath); + var found = GetXml(umbracoContext).SelectSingleNode(xpath, vars); // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo // but maybe that was the url of a non-default top-level node, so we also // have to look for /foo (see note in NiceUrlProvider). if (found == null && hideTopLevelNode.Value && path.Length > 1 && path.IndexOf('/', 1) < 0) { - xpath = CreateXpathQuery(startNodeId, path, false); - found = GetXml(umbracoContext).SelectSingleNode(xpath); + xpath = CreateXpathQuery(startNodeId, path, false, out vars); + found = GetXml(umbracoContext).SelectSingleNode(xpath, vars); } return ConvertToDocument(found); @@ -152,11 +154,19 @@ namespace Umbraco.Web if (rootNodeId > 0) xpathBuilder.AppendFormat(XPathStrings.DescendantDocumentById, rootNodeId); + + XPathVariable var = null; + if (alias.Contains('\'') || alias.Contains('"')) + { + // use a var, escaping gets ugly pretty quickly + var = new XPathVariable("alias", alias); + alias = "$alias"; + } xpathBuilder.AppendFormat(XPathStrings.DescendantDocumentByAlias, alias); var xpath = xpathBuilder.ToString(); - return ConvertToDocument(GetXml(umbracoContext).SelectSingleNode(xpath)); + return ConvertToDocument(GetXml(umbracoContext).SelectSingleNode(xpath, var)); } public bool HasContent(UmbracoContext umbracoContext) @@ -177,9 +187,10 @@ namespace Umbraco.Web static readonly char[] SlashChar = new char[] { '/' }; - protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath) + protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable vars) { string xpath; + vars = null; if (path == string.Empty || path == "/") { @@ -213,6 +224,7 @@ namespace Umbraco.Web var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); var xpathBuilder = new StringBuilder(); int partsIndex = 0; + List varsList = null; if (startNodeId == 0) { @@ -229,7 +241,16 @@ namespace Umbraco.Web while (partsIndex < urlParts.Length) { - xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, urlParts[partsIndex++]); + var part = urlParts[partsIndex++]; + if (part.Contains('\'') || part.Contains('"')) + { + // use vars, escaping gets ugly pretty quickly + varsList = varsList ?? new List(); + var varName = string.Format("var{0}", partsIndex); + varsList.Add(new XPathVariable(varName, part)); + part = "$" + varName; + } + xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part); } xpath = xpathBuilder.ToString();