using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
namespace Umbraco.Core
{
///
/// The XmlHelper class contains general helper methods for working with xml in umbraco.
///
public class XmlHelper
{
///
/// Creates or sets an attribute on the XmlNode if an Attributes collection is available
///
///
///
///
///
public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value)
{
if (xml == null) throw new ArgumentNullException("xml");
if (n == null) throw new ArgumentNullException("n");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
if (n.Attributes == null)
{
return;
}
if (n.Attributes[name] == null)
{
var a = xml.CreateAttribute(name);
a.Value = value;
n.Attributes.Append(a);
}
else
{
n.Attributes[name].Value = value;
}
}
///
/// Gets a value indicating whether a specified string contains only xml whitespace characters.
///
/// The string.
/// true if the string contains only xml whitespace characters.
/// As per XML 1.1 specs, space, \t, \r and \n.
public static bool IsXmlWhitespace(string s)
{
// as per xml 1.1 specs - anything else is significant whitespace
s = s.Trim(' ', '\t', '\r', '\n');
return s.Length == 0;
}
///
/// Creates a new XPathDocument from an xml string.
///
/// The xml string.
/// An XPathDocument created from the xml string.
public static XPathDocument CreateXPathDocument(string xml)
{
return new XPathDocument(new XmlTextReader(new StringReader(xml)));
}
///
/// Tries to create a new XPathDocument from an xml string.
///
/// The xml string.
/// The XPath document.
/// A value indicating whether it has been possible to create the document.
public static bool TryCreateXPathDocument(string xml, out XPathDocument doc)
{
try
{
doc = CreateXPathDocument(xml);
return true;
}
catch (Exception)
{
doc = null;
return false;
}
}
///
/// Tries to create a new XPathDocument from a property value.
///
/// The value of the property.
/// The XPath document.
/// A value indicating whether it has been possible to create the document.
/// The value can be anything... Performance-wise, this is bad.
public static bool TryCreateXPathDocumentFromPropertyValue(object value, out XPathDocument doc)
{
// DynamicNode.ConvertPropertyValueByDataType first cleans the value by calling
// XmlHelper.StripDashesInElementOrAttributeName - this is because the XML is
// to be returned as a DynamicXml and element names such as "value-item" are
// invalid and must be converted to "valueitem". But we don't have that sort of
// problem here - and we don't need to bother with dashes nor dots, etc.
doc = null;
var xml = value as string;
if (xml == null) return false; // no a string
if (CouldItBeXml(xml) == false) return false; // string does not look like it's xml
if (IsXmlWhitespace(xml)) return false; // string is whitespace, xml-wise
if (TryCreateXPathDocument(xml, out doc) == false) return false; // string can't be parsed into xml
var nav = doc.CreateNavigator();
if (nav.MoveToFirstChild())
{
var name = nav.LocalName; // must not match an excluded tag
if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true;
}
doc = null;
return false;
}
///
/// Tries to create a new XElement from a property value.
///
/// The value of the property.
/// The Xml element.
/// A value indicating whether it has been possible to create the element.
/// The value can be anything... Performance-wise, this is bad.
public static bool TryCreateXElementFromPropertyValue(object value, out XElement elt)
{
// see note above in TryCreateXPathDocumentFromPropertyValue...
elt = null;
var xml = value as string;
if (xml == null) return false; // not a string
if (CouldItBeXml(xml) == false) return false; // string does not look like it's xml
if (IsXmlWhitespace(xml)) return false; // string is whitespace, xml-wise
try
{
elt = XElement.Parse(xml, LoadOptions.None);
}
catch
{
elt = null;
return false; // string can't be parsed into xml
}
var name = elt.Name.LocalName; // must not match an excluded tag
if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true;
elt = null;
return false;
}
///
/// Sorts the children of a parentNode.
///
/// The parent node.
/// An XPath expression to select children of to sort.
/// A function returning the value to order the nodes by.
internal static void SortNodes(
XmlNode parentNode,
string childNodesXPath,
Func orderBy)
{
var sortedChildNodes = parentNode.SelectNodes(childNodesXPath).Cast()
.OrderBy(orderBy)
.ToArray();
// append child nodes to last position, in sort-order
// so all child nodes will go after the property nodes
foreach (var node in sortedChildNodes)
parentNode.AppendChild(node); // moves the node to the last position
}
///
/// Sorts the children of a parentNode if needed.
///
/// The parent node.
/// An XPath expression to select children of to sort.
/// A function returning the value to order the nodes by.
/// A value indicating whether sorting was needed.
/// same as SortNodes but will do nothing if nodes are already sorted - should improve performances.
internal static bool SortNodesIfNeeded(
XmlNode parentNode,
string childNodesXPath,
Func orderBy)
{
// ensure orderBy runs only once per node
// checks whether nodes are already ordered
// and actually sorts only if needed
var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath).Cast()
.Select(x => Tuple.Create(x, orderBy(x))).ToArray();
var a = 0;
foreach (var x in childNodesAndOrder)
{
if (a > x.Item2)
{
a = -1;
break;
}
a = x.Item2;
}
if (a >= 0)
return false;
// append child nodes to last position, in sort-order
// so all child nodes will go after the property nodes
foreach (var x in childNodesAndOrder.OrderBy(x => x.Item2))
parentNode.AppendChild(x.Item1); // moves the node to the last position
return true;
}
///
/// Sorts a single child node of a parentNode.
///
/// The parent node.
/// An XPath expression to select children of to sort.
/// The child node to sort.
/// A function returning the value to order the nodes by.
/// A value indicating whether sorting was needed.
/// Assuming all nodes but are sorted, this will move the node to
/// the right position without moving all the nodes (as SortNodes would do) - should improve perfs.
internal static bool SortNode(
XmlNode parentNode,
string childNodesXPath,
XmlNode node,
Func orderBy)
{
var nodeSortOrder = orderBy(node);
var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath).Cast()
.Select(x => Tuple.Create(x, orderBy(x))).ToArray();
// only one node = node is in the right place already, obviously
if (childNodesAndOrder.Length == 1) return false;
// find the first node with a sortOrder > node.sortOrder
var i = 0;
while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder)
i++;
// if one was found
if (i < childNodesAndOrder.Length)
{
// and node is just before, we're done already
// else we need to move it right before the node that was found
if (i == 0 || childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.InsertBefore(node, childNodesAndOrder[i].Item1);
return true;
}
}
else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1
{
// and node is the last one, we're done already
// else we need to append it as the last one
// (and i > 1, see above)
if (childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.AppendChild(node);
return true;
}
}
return false;
}
// used by DynamicNode only, see note in TryCreateXPathDocumentFromPropertyValue
public static string StripDashesInElementOrAttributeNames(string xml)
{
using (var outputms = new MemoryStream())
{
using (TextWriter outputtw = new StreamWriter(outputms))
{
using (var ms = new MemoryStream())
{
using (var tw = new StreamWriter(ms))
{
tw.Write(xml);
tw.Flush();
ms.Position = 0;
using (var tr = new StreamReader(ms))
{
bool IsInsideElement = false, IsInsideQuotes = false;
int ic = 0;
while ((ic = tr.Read()) != -1)
{
if (ic == (int)'<' && !IsInsideQuotes)
{
if (tr.Peek() != (int)'!')
{
IsInsideElement = true;
}
}
if (ic == (int)'>' && !IsInsideQuotes)
{
IsInsideElement = false;
}
if (ic == (int)'"')
{
IsInsideQuotes = !IsInsideQuotes;
}
if (!IsInsideElement || ic != (int)'-' || IsInsideQuotes)
{
outputtw.Write((char)ic);
}
}
}
}
}
outputtw.Flush();
outputms.Position = 0;
using (TextReader outputtr = new StreamReader(outputms))
{
return outputtr.ReadToEnd();
}
}
}
}
///
/// Imports a XML node from text.
///
/// The text.
/// The XML doc.
///
public static XmlNode ImportXmlNodeFromText(string text, ref XmlDocument xmlDoc)
{
xmlDoc.LoadXml(text);
return xmlDoc.FirstChild;
}
///
/// Opens a file as a XmlDocument.
///
/// The relative file path. ei. /config/umbraco.config
/// Returns a XmlDocument class
public static XmlDocument OpenAsXmlDocument(string filePath)
{
var reader = new XmlTextReader(IOHelper.MapPath(filePath)) {WhitespaceHandling = WhitespaceHandling.All};
var xmlDoc = new XmlDocument();
//Load the file into the XmlDocument
xmlDoc.Load(reader);
//Close off the connection to the file.
reader.Close();
return xmlDoc;
}
///
/// creates a XmlAttribute with the specified name and value
///
/// The xmldocument.
/// The name of the attribute.
/// The value of the attribute.
/// a XmlAttribute
public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value)
{
if (xd == null) throw new ArgumentNullException("xd");
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Value cannot be null or empty.", "name");
var temp = xd.CreateAttribute(name);
temp.Value = value;
return temp;
}
///
/// Creates a text XmlNode with the specified name and value
///
/// The xmldocument.
/// The node name.
/// The node value.
/// a XmlNode
public static XmlNode AddTextNode(XmlDocument xd, string name, string value)
{
if (xd == null) throw new ArgumentNullException("xd");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
var temp = xd.CreateNode(XmlNodeType.Element, name, "");
temp.AppendChild(xd.CreateTextNode(value));
return temp;
}
///
/// Sets or Creates a text XmlNode with the specified name and value
///
/// The xmldocument.
/// The node to set or create the child text node on
/// The node name.
/// The node value.
/// a XmlNode
public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value)
{
if (xd == null) throw new ArgumentNullException("xd");
if (parent == null) throw new ArgumentNullException("parent");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
var child = parent.SelectSingleNode(name);
if (child != null)
{
child.InnerText = value;
return child;
}
return AddTextNode(xd, name, value);
}
///
/// Creates a cdata XmlNode with the specified name and value
///
/// The xmldocument.
/// The node name.
/// The node value.
/// A XmlNode
public static XmlNode AddCDataNode(XmlDocument xd, string name, string value)
{
if (xd == null) throw new ArgumentNullException("xd");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
var temp = xd.CreateNode(XmlNodeType.Element, name, "");
temp.AppendChild(xd.CreateCDataSection(value));
return temp;
}
///
/// Sets or Creates a cdata XmlNode with the specified name and value
///
/// The xmldocument.
/// The node to set or create the child text node on
/// The node name.
/// The node value.
/// a XmlNode
public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value)
{
if (xd == null) throw new ArgumentNullException("xd");
if (parent == null) throw new ArgumentNullException("parent");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
var child = parent.SelectSingleNode(name);
if (child != null)
{
child.InnerXml = ""; ;
return child;
}
return AddCDataNode(xd, name, value);
}
///
/// Gets the value of a XmlNode
///
/// The XmlNode.
/// the value as a string
public static string GetNodeValue(XmlNode n)
{
var value = string.Empty;
if (n == null || n.FirstChild == null)
return value;
value = n.FirstChild.Value ?? n.InnerXml;
return value.Replace("", "", "]]>");
}
///
/// Determines whether the specified string appears to be XML.
///
/// The XML string.
///
/// true if the specified string appears to be XML; otherwise, false.
///
public static bool CouldItBeXml(string xml)
{
if (string.IsNullOrEmpty(xml)) return false;
xml = xml.Trim();
return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/');
}
///
/// Splits the specified delimited string into an XML document.
///
/// The data.
/// The separator.
/// Name of the root.
/// Name of the element.
/// Returns an System.Xml.XmlDocument representation of the delimited string data.
public static XmlDocument Split(string data, string[] separator, string rootName, string elementName)
{
return Split(new XmlDocument(), data, separator, rootName, elementName);
}
///
/// Splits the specified delimited string into an XML document.
///
/// The XML document.
/// The delimited string data.
/// The separator.
/// Name of the root node.
/// Name of the element node.
/// Returns an System.Xml.XmlDocument representation of the delimited string data.
public static XmlDocument Split(XmlDocument xml, string data, string[] separator, string rootName, string elementName)
{
// load new XML document.
xml.LoadXml(string.Concat("<", rootName, "/>"));
// get the data-value, check it isn't empty.
if (!string.IsNullOrEmpty(data))
{
// explode the values into an array
var values = data.Split(separator, StringSplitOptions.None);
// loop through the array items.
foreach (string value in values)
{
// add each value to the XML document.
var xn = XmlHelper.AddTextNode(xml, elementName, value);
xml.DocumentElement.AppendChild(xn);
}
}
// return the XML node.
return xml;
}
///
/// Return a dictionary of attributes found for a string based tag
///
///
///
public static Dictionary GetAttributesFromElement(string tag)
{
var m =
Regex.Matches(tag, "(?\\S*)=\"(?[^\"]*)\"",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
// fix for issue 14862: return lowercase attributes for case insensitive matching
var d = m.Cast().ToDictionary(attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), attributeSet => attributeSet.Groups["attributeValue"].Value.ToString());
return d;
}
}
}