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();