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