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 { /// /// 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 the parentNode that match the xpath selector /// /// /// An xpath expression used to select child nodes of the XmlElement /// An expression that returns true if the XElement passed in is a valid child node to be sorted /// The value to order the results by internal static void SortNodes( XmlNode parentNode, string childXPathSelector, Func childSelector, Func orderByValue) { var xElement = parentNode.ToXElement(); var children = xElement.Elements().Where(x => childSelector(x)).ToArray(); //(DONT conver to method group, the build server doesn't like it) var data = children .OrderByDescending(orderByValue) //order by the sort order desc .Select(x => children.IndexOf(x)) //store the current item's index (DONT conver to method group, the build server doesn't like it) .ToList(); //get the minimum index that a content node exists in the parent var minElementIndex = xElement.Elements() .TakeWhile(x => childSelector(x) == false) .Count(); //if the minimum index is zero, then it is the very first node inside the parent, // if it is not, we need to store the child property node that exists just before the // first content node found so we can insert elements after it when we're sorting. var insertAfter = minElementIndex == 0 ? null : parentNode.ChildNodes[minElementIndex - 1]; var selectedChildren = parentNode.SelectNodes(childXPathSelector); if (selectedChildren == null) { throw new InvalidOperationException(string.Format("The childXPathSelector value did not return any results {0}", childXPathSelector)); } var childElements = selectedChildren.Cast().ToArray(); //iterate over the ndoes starting with the node with the highest sort order. //then we insert this node at the begginning of the parent so that by the end //of the iteration the node with the least sort order will be at the top. foreach (var node in data.Select(index => childElements[index])) { if (insertAfter == null) { parentNode.PrependChild(node); } else { parentNode.InsertAfter(node, insertAfter); } } } // 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) { 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) { var temp = xd.CreateNode(XmlNodeType.Element, name, ""); temp.AppendChild(xd.CreateTextNode(value)); return temp; } /// /// 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) { var temp = xd.CreateNode(XmlNodeType.Element, name, ""); temp.AppendChild(xd.CreateCDataSection(value)); return temp; } /// /// 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; } } }