U4-1611 - fix xpath special chars escaping issues in published content store

This commit is contained in:
Stephan
2013-02-14 16:23:56 -01:00
parent 6a6b82d1e8
commit 8de579083c
7 changed files with 631 additions and 7 deletions

View File

@@ -724,6 +724,11 @@
<Compile Include="WriteLock.cs" />
<Compile Include="XmlExtensions.cs" />
<Compile Include="XmlHelper.cs" />
<Compile Include="Xml\DynamicContext.cs" />
<Compile Include="Xml\XmlNamespaces.cs" />
<Compile Include="Xml\XmlNodeListFactory.cs" />
<Compile Include="Xml\XmlNodeExtensions.cs" />
<Compile Include="Xml\XPathVariable.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -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
{
/// <summary>
/// Provides the evaluation context for fast execution and custom
/// variables resolution.
/// </summary>
/// <remarks>
/// This class is responsible for resolving variables during dynamic expression execution.
/// <para>Discussed in http://weblogs.asp.net/cazzu/archive/2003/10/07/30888.aspx</para>
/// <para>Author: Daniel Cazzulino, <a href="http://clariusconsulting.net/kzu">blog</a></para>
/// </remarks>
public class DynamicContext : XsltContext
{
#region Private vars
readonly IDictionary<string, IXsltContextVariable> _variables =
new Dictionary<string, IXsltContextVariable>();
#endregion Private
#region Constructors & Initialization
/// <summary>
/// Initializes a new instance of the <see cref="DynamicContext"/> class.
/// </summary>
public DynamicContext()
: base(new NameTable())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DynamicContext"/>
/// class with the specified <see cref="NameTable"/>.
/// </summary>
/// <param name="table">The NameTable to use.</param>
public DynamicContext(NameTable table)
: base(table)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DynamicContext"/> class.
/// </summary>
/// <param name="context">A previously filled context with the namespaces to use.</param>
public DynamicContext(XmlNamespaceManager context)
: this(context, new NameTable())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DynamicContext"/> class.
/// </summary>
/// <param name="context">A previously filled context with the namespaces to use.</param>
/// <param name="table">The NameTable to use.</param>
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
/// <summary>
/// Implementation equal to <see cref="XsltContext"/>.
/// </summary>
public override int CompareDocument(string baseUri, string nextbaseUri)
{
return String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture);
}
/// <summary>
/// Same as <see cref="XmlNamespaceManager"/>.
/// </summary>
public override string LookupNamespace(string prefix)
{
var key = NameTable.Get(prefix);
return key == null ? null : base.LookupNamespace(key);
}
/// <summary>
/// Same as <see cref="XmlNamespaceManager"/>.
/// </summary>
public override string LookupPrefix(string uri)
{
var key = NameTable.Get(uri);
return key == null ? null : base.LookupPrefix(key);
}
/// <summary>
/// Same as <see cref="XsltContext"/>.
/// </summary>
public override bool PreserveWhitespace(XPathNavigator node)
{
return true;
}
/// <summary>
/// Same as <see cref="XsltContext"/>.
/// </summary>
public override bool Whitespace
{
get { return true; }
}
#endregion Common Overrides
#region Public Members
/// <summary>
/// Shortcut method that compiles an expression using an empty navigator.
/// </summary>
/// <param name="xpath">The expression to compile</param>
/// <returns>A compiled <see cref="XPathExpression"/>.</returns>
public static XPathExpression Compile(string xpath)
{
return new XmlDocument().CreateNavigator().Compile(xpath);
}
#endregion Public Members
#region Variable Handling Code
/// <summary>
/// Adds the variable to the dynamic evaluation context.
/// </summary>
/// <param name="name">The name of the variable to add to the context.</param>
/// <param name="value">The value of the variable to add to the context.</param>
/// <remarks>
/// Value type conversion for XPath evaluation is as follows:
/// <list type="table">
/// <listheader>
/// <term>CLR Type</term>
/// <description>XPath type</description>
/// </listheader>
/// <item>
/// <term>System.String</term>
/// <description>XPathResultType.String</description>
/// </item>
/// <item>
/// <term>System.Double (or types that can be converted to)</term>
/// <description>XPathResultType.Number</description>
/// </item>
/// <item>
/// <term>System.Boolean</term>
/// <description>XPathResultType.Boolean</description>
/// </item>
/// <item>
/// <term>System.Xml.XPath.XPathNavigator</term>
/// <description>XPathResultType.Navigator</description>
/// </item>
/// <item>
/// <term>System.Xml.XPath.XPathNodeIterator</term>
/// <description>XPathResultType.NodeSet</description>
/// </item>
/// <item>
/// <term>Others</term>
/// <description>XPathResultType.Any</description>
/// </item>
/// </list>
/// <note type="note">See the topic "Compile, Select, Evaluate, and Matches with
/// XPath and XPathExpressions" in MSDN documentation for additional information.</note>
/// </remarks>
/// <exception cref="ArgumentNullException">The <paramref name="value"/> is null.</exception>
public void AddVariable(string name, object value)
{
if (value == null) throw new ArgumentNullException("value");
_variables[name] = new DynamicVariable(name, value);
}
/// <summary>
/// See <see cref="XsltContext"/>. Not used in our implementation.
/// </summary>
public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] argTypes)
{
return null;
}
/// <summary>
/// Resolves the dynamic variables added to the context. See <see cref="XsltContext"/>.
/// </summary>
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
/// <summary>
/// Represents a variable during dynamic expression execution.
/// </summary>
internal class DynamicVariable : IXsltContextVariable
{
readonly string _name;
readonly object _value;
#region Public Members
public string Name { get { return _name; } }
/// <summary>
/// Initializes a new instance of the class.
/// </summary>
/// <param name="name">The name of the variable.</param>
/// <param name="value">The value of the variable.</param>
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
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,41 @@
// source: mvpxml.codeplex.com
namespace Umbraco.Core.Xml
{
/// <summary>
/// Provides public constants for wellknown XML namespaces.
/// </summary>
/// <remarks>Author: Daniel Cazzulino, <a href="http://clariusconsulting.net/kzu">blog</a></remarks>
public static class XmlNamespaces
{
/// <summary>
/// The public XML 1.0 namespace.
/// </summary>
/// <remarks>See http://www.w3.org/TR/2004/REC-xml-20040204/</remarks>
public const string Xml = "http://www.w3.org/XML/1998/namespace";
/// <summary>
/// Public Xml Namespaces specification namespace.
/// </summary>
/// <remarks>See http://www.w3.org/TR/REC-xml-names/</remarks>
public const string XmlNs = "http://www.w3.org/2000/xmlns/";
/// <summary>
/// Public Xml Namespaces prefix.
/// </summary>
/// <remarks>See http://www.w3.org/TR/REC-xml-names/</remarks>
public const string XmlNsPrefix = "xmlns";
/// <summary>
/// XML Schema instance namespace.
/// </summary>
/// <remarks>See http://www.w3.org/TR/xmlschema-1/</remarks>
public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance";
/// <summary>
/// XML 1.0 Schema namespace.
/// </summary>
/// <remarks>See http://www.w3.org/TR/xmlschema-1/</remarks>
public const string Xsd = "http://www.w3.org/2001/XMLSchema";
}
}

View File

@@ -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<XPathVariable> 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<XPathVariable> 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<XmlNode>().FirstOrDefault();
}
}
}

View File

@@ -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
/// <summary>
/// Creates an instance of a <see cref="XmlNodeList"/> that allows
/// enumerating <see cref="XmlNode"/> elements in the iterator.
/// </summary>
/// <param name="iterator">The result of a previous node selection
/// through an <see cref="XPathNavigator"/> query.</param>
/// <returns>An initialized list ready to be enumerated.</returns>
/// <remarks>The underlying XML store used to issue the query must be
/// an object inheriting <see cref="XmlNode"/>, such as
/// <see cref="XmlDocument"/>.</remarks>
public static XmlNodeList CreateNodeList(XPathNodeIterator iterator)
{
return new XmlNodeListIterator(iterator);
}
#endregion Public members
#region XmlNodeListIterator
private class XmlNodeListIterator : XmlNodeList
{
readonly XPathNodeIterator _iterator;
readonly IList<XmlNode> _nodes = new List<XmlNode>();
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;
}
}
/// <summary>
/// Reads the entire iterator.
/// </summary>
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;
}
/// <summary>
/// Reads up to the specified index, or until the
/// iterator is consumed.
/// </summary>
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;
}
}
}
/// <summary>
/// Flags that the iterator has been consumed.
/// </summary>
private bool Done
{
get { return _done; }
}
bool _done;
/// <summary>
/// Current count of nodes in the iterator (read so far).
/// </summary>
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
}
}

View File

@@ -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<XPathVariable> 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<XPathVariable> 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<XPathVariable> 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<XPathVariable>();
var varName = string.Format("var{0}", partsIndex);
varsList.Add(new XPathVariable(varName, part));
part = "$" + varName;
}
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part);
}
xpath = xpathBuilder.ToString();