Is there anyone who wonder how you can perform an XPath query when you need to find a XML node with an attribute that contains single quote and double quote symbols together? For example,
<Sample Text="Sunghwa's thought about "Matrix Revolution"" />
The problem here is that XPath 1.0 standard doesn't mention how you can escape single quote or double quote in literal. According to the standard it just define literal as:
Literal ::= '"' [^"]* '"' | "'" [^'] "'"
Some of you may think that you can simply do this:
doc.SelectSingleNode("//Sample[@Text=\"Sunghwa's thought about "Matrix Revolution"\"]");
But the problem is, as defined in the XPath 1.0 standard, the literal string is any string enclosed by single quote symbol except the single quote itself or any string enclosed by double quote symbol except the double quote. So " is interpreted as it is.
Some people didn't give up and use solution like below:
doc.selectSingleNode("//Sample[@Text=concat(\"Sunghwa's thought about \", '\"Matrix Revolution\"')]");
But I believe this isn't attractive solution to most of you.
XPath injection is also the same concern when you need to build your XPath based on user input like below:
doc.selectSingleNode("//User[@Name='" + userName + "']");
So what's solution here?
Before I decided to post this article, I actually found an article from Cazzu's blog which already implemented this, but I thought it would be interesting to show some points that I want to discuss in simple code rather than more complecated MVP XML Library. I recommend this library although I didn't have chance to use it.
For those who want to see the simple code rather than using the library, I wrote my own sample using the same technique I posted in my article about case-insensitive XPath.
The following is the sample code that shows how clean the code is after applying it.
CustomContext ctx = new CustomContext((NameTable)doc.NameTable);
ctx.AddParam("name", @" Sunghwa's thought about \"Matrix Revolution\"");
XmlNodeList nodes = doc.SelectNodes("//Sample[@Name = $name]", ctx);
The full source code is available below. This sample has been modifed from my previous posting's sample:
using System;using System.Xml;using System.Xml.Xsl;using System.Xml.XPath;
namespace CaseInsensitiveXPathTest{ class Program { static void Main(string[] args) { XmlDocument doc = new XmlDocument(); doc.LoadXml("<Lists><List Name=\"A'"BC\"/><List Name=\"abc\"/><List Name=\"123\"/></Lists>");
ctx.AddNamespace("s", "http://shjin.local/"); ctx.AddParam("name", @"A'""BC");
XmlNodeList nodes = doc.SelectNodes("//List[s:equals(@Name, $name)]", ctx);
foreach (XmlNode node in nodes) { System.Diagnostics.Debug.WriteLine(node.OuterXml); } } }
class CustomContext : System.Xml.Xsl.XsltContext { private XsltArgumentList m_argumentList = new XsltArgumentList();
public CustomContext() : base() { }
public CustomContext(NameTable table) : base(table) { }
public override bool Whitespace { get { return true; } }
public override int CompareDocument(string baseUri, string nextbaseUri) { return 0; }
public override bool PreserveWhitespace(System.Xml.XPath.XPathNavigator node) { return true; }
public void AddParam(string name, object parameter) { m_argumentList.AddParam(name, string.Empty, parameter); }
public object GetParam(string name) { return m_argumentList.GetParam(name, string.Empty); }
public object RemoveParam(string name) { return m_argumentList.RemoveParam(name, string.Empty); }
public override IXsltContextFunction ResolveFunction(string prefix, string name, System.Xml.XPath.XPathResultType[] ArgTypes) { IXsltContextFunction resolvedFunction = null;
if (this.LookupNamespace(prefix) == EqualsFunction.Namespace && name == EqualsFunction.FunctionName) { resolvedFunction = new EqualsFunction(); }
return resolvedFunction; }
public override IXsltContextVariable ResolveVariable(string prefix, string name) { IXsltContextVariable contextVariable;
object parameter = this.GetParam(name);
if (parameter != null) { contextVariable = new ContextVariable(name, parameter); } else { contextVariable = null; }
return contextVariable; }
private class ContextVariable : IXsltContextVariable { private string m_name; private object m_parameter;
public ContextVariable(string name, object parameter) { m_name = name; m_parameter = parameter; }
#region IXsltContextVariable Members
public object Evaluate(XsltContext xsltContext) { return m_parameter; }
public bool IsLocal { get { return true; } }
public bool IsParam { get { return true; } }
public XPathResultType VariableType { get { return XPathResultType.Any; } }
#endregion }
private class EqualsFunction : IXsltContextFunction { public const string Namespace = "http://shjin.local/"; public const string FunctionName = "equals";
private System.Xml.XPath.XPathResultType[] m_argTypes = new System.Xml.XPath.XPathResultType[] { System.Xml.XPath.XPathResultType.Any, System.Xml.XPath.XPathResultType.Any };
#region IXsltContextFunction Members
public System.Xml.XPath.XPathResultType[] ArgTypes { get { return m_argTypes; } }
private string GetStringFromInvokeArgument(object arg) { string stringValue;
if (arg is string) { stringValue = (string)(arg); } else if (arg is XPathNodeIterator) { XPathNodeIterator i = (XPathNodeIterator)arg;
if (i.MoveNext() == true) { XPathNavigator navigator = i.Current; stringValue = navigator.ToString(); } else { throw new ArgumentException("One of argument doesn't expand as string."); } } else { throw new ArgumentException("equals() only support string or node path"); }
return stringValue; }
public object Invoke(XsltContext xsltContext, object[] args, System.Xml.XPath.XPathNavigator docContext) { if (args.Length != 2) { throw new ArgumentException("equals() only takes two arguments"); }
string argument1 = GetStringFromInvokeArgument(args[0]); string argument2 = GetStringFromInvokeArgument(args[1]);
bool result = string.Equals(argument1, argument2, StringComparison.CurrentCultureIgnoreCase);
return result; }
public int Maxargs { get { return 2; } }
public int Minargs { get { return 2; } }
public System.Xml.XPath.XPathResultType ReturnType { get { return System.Xml.XPath.XPathResultType.Boolean; } }
#endregion } }}
This posting is provided "AS IS" with no warranties, and confers no rights.