blob: b3119978ed9e1e15f1b75624767a063935babd1c [file] [log] [blame]
/*******************************************************************************
* You may amend and distribute as you like, but don't remove this header!
*
* EPPlus provides server-side generation of Excel 2007/2010 spreadsheets.
* See http://www.codeplex.com/EPPlus for details.
*
* Copyright (C) 2011 Jan Källman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
* If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
*
* All code and executables are provided "as is" with no warranty either express or implied.
* The author accepts no liability for any damage or loss of business that this product may cause.
*
* Code change notes:
*
* Author Change Date
* ******************************************************************************
* Jan Källman Initial Release 2009-10-01
* Jan Källman License changed GPL-->LGPL 2011-12-27
* Eyal Seagull Add "CreateComplexNode" 2012-04-03
* Eyal Seagull Add "DeleteTopNode" 2012-04-13
*******************************************************************************/
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using OfficeOpenXml.Style;
namespace OfficeOpenXml;
/// <summary>
/// Help class containing XML functions.
/// Can be Inherited
/// </summary>
public abstract class XmlHelper {
internal delegate int ChangedEventHandler(StyleBase sender, StyleChangeEventArgs e);
internal XmlHelper(XmlNamespaceManager nameSpaceManager) {
TopNode = null;
NameSpaceManager = nameSpaceManager;
}
internal XmlHelper(XmlNamespaceManager nameSpaceManager, XmlNode topNode) {
TopNode = topNode;
NameSpaceManager = nameSpaceManager;
}
//internal bool ChangedFlag;
internal XmlNamespaceManager NameSpaceManager { get; set; }
internal XmlNode TopNode { get; set; }
/// <summary>
/// Schema order list
/// </summary>
protected virtual ImmutableArray<string> SchemaNodeOrder => ImmutableArray<string>.Empty;
internal XmlNode CreateNode(string path) {
if (path == "") {
return TopNode;
}
return CreateNode(path, false);
}
internal XmlNode CreateNode(string path, bool insertFirst) {
XmlNode node = TopNode;
XmlNode prependNode = null;
foreach (string subPath in path.Split('/')) {
XmlNode subNode = node.SelectSingleNode(subPath, NameSpaceManager);
if (subNode == null) {
string nodeName;
string nodePrefix;
string nameSpaceUri;
string[] nameSplit = subPath.Split(':');
if (SchemaNodeOrder.Length > 0 && subPath[0] != '@') {
insertFirst = false;
prependNode = GetPrependNode(subPath, node);
}
if (nameSplit.Length > 1) {
nodePrefix = nameSplit[0];
if (nodePrefix[0] == '@') {
nodePrefix = nodePrefix.Substring(1, nodePrefix.Length - 1);
}
nameSpaceUri = NameSpaceManager.LookupNamespace(nodePrefix);
nodeName = nameSplit[1];
} else {
nodePrefix = "";
nameSpaceUri = "";
nodeName = nameSplit[0];
}
if (subPath.StartsWith("@")) {
XmlAttribute addedAtt = node.OwnerDocument.CreateAttribute(
subPath.Substring(1, subPath.Length - 1),
nameSpaceUri); //nameSpaceURI
node.Attributes.Append(addedAtt);
} else {
if (nodePrefix == "") {
subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri);
} else {
if (nodePrefix == ""
|| (node.OwnerDocument != null
&& node.OwnerDocument.DocumentElement != null
&& node.OwnerDocument.DocumentElement.NamespaceURI == nameSpaceUri
&& node.OwnerDocument.DocumentElement.Prefix == "")) {
subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri);
} else {
subNode = node.OwnerDocument.CreateElement(nodePrefix, nodeName, nameSpaceUri);
}
}
if (prependNode != null) {
node.InsertBefore(subNode, prependNode);
prependNode = null;
} else if (insertFirst) {
node.PrependChild(subNode);
} else {
node.AppendChild(subNode);
}
}
}
node = subNode;
}
return node;
}
/// <summary>
/// Options to insert a node in the XmlDocument
/// </summary>
internal enum eNodeInsertOrder {
/// <summary>
/// Insert as first node of "topNode"
/// </summary>
First,
/// <summary>
/// Insert as the last child of "topNode"
/// </summary>
Last,
/// <summary>
/// Insert after the "referenceNode"
/// </summary>
After,
/// <summary>
/// Insert before the "referenceNode"
/// </summary>
Before,
/// <summary>
/// Use the Schema List to insert in the right order. If the Schema list
/// is null or empty, consider "Last" as the selected option
/// </summary>
SchemaOrder,
}
/// <summary>
/// Create a complex node. Insert the node according to the <paramref name="path"/>
/// using the <paramref name="topNode"/> as the parent
/// </summary>
/// <param name="topNode"></param>
/// <param name="path"></param>
/// <returns></returns>
internal XmlNode CreateComplexNode(XmlNode topNode, string path) {
return CreateComplexNode(topNode, path, eNodeInsertOrder.SchemaOrder, null);
}
/// <summary>
/// Creates complex XML nodes
/// </summary>
/// <remarks>
/// 1. "d:conditionalFormatting"
/// 1.1. Creates/find the first "conditionalFormatting" node
///
/// 2. "d:conditionalFormatting/@sqref"
/// 2.1. Creates/find the first "conditionalFormatting" node
/// 2.2. Creates (if not exists) the @sqref attribute
///
/// 3. "d:conditionalFormatting/@id='7'/@sqref='A9:B99'"
/// 3.1. Creates/find the first "conditionalFormatting" node
/// 3.2. Creates/update its @id attribute to "7"
/// 3.3. Creates/update its @sqref attribute to "A9:B99"
///
/// 4. "d:conditionalFormatting[@id='7']/@sqref='X1:X5'"
/// 4.1. Creates/find the first "conditionalFormatting" node with @id=7
/// 4.2. Creates/update its @sqref attribute to "X1:X5"
///
/// 5. "d:conditionalFormatting[@id='7']/@id='8'/@sqref='X1:X5'/d:cfRule/@id='AB'"
/// 5.1. Creates/find the first "conditionalFormatting" node with @id=7
/// 5.2. Set its @id attribute to "8"
/// 5.2. Creates/update its @sqref attribute and set it to "X1:X5"
/// 5.3. Creates/find the first "cfRule" node (inside the node)
/// 5.4. Creates/update its @id attribute to "AB"
///
/// 6. "d:cfRule/@id=''"
/// 6.1. Creates/find the first "cfRule" node
/// 6.1. Remove the @id attribute
/// </remarks>
/// <param name="topNode"></param>
/// <param name="path"></param>
/// <param name="nodeInsertOrder"></param>
/// <param name="referenceNode"></param>
/// <returns>The last node creates/found</returns>
internal XmlNode CreateComplexNode(
XmlNode topNode,
string path,
eNodeInsertOrder nodeInsertOrder,
XmlNode referenceNode) {
// Path is obrigatory
if ((path == null) || (path == string.Empty)) {
return topNode;
}
XmlNode node = topNode;
//TODO: BUG: when the "path" contains "/" in an attrribue value, it gives an error.
// Separate the XPath to Nodes and Attributes
foreach (string subPath in path.Split('/')) {
// The subPath can be any one of those:
// nodeName
// x:nodeName
// nodeName[find criteria]
// x:nodeName[find criteria]
// @attribute
// @attribute='attribute value'
// Check if the subPath has at least one character
if (subPath.Length > 0) {
// Check if the subPath is an attribute (with or without value)
if (subPath.StartsWith("@")) {
// @attribute --> Create attribute
// @attribute='' --> Remove attribute
// @attribute='attribute value' --> Create attribute + update value
string[] attributeSplit = subPath.Split('=');
string attributeName = attributeSplit[0].Substring(1, attributeSplit[0].Length - 1);
string attributeValue = null; // Null means no attribute value
// Check if we have an attribute value to set
if (attributeSplit.Length > 1) {
// Remove the ' or " from the attribute value
attributeValue = attributeSplit[1].Replace("'", "").Replace("\"", "");
}
// Get the attribute (if exists)
XmlAttribute attribute = (XmlAttribute)(node.Attributes.GetNamedItem(attributeName));
// Remove the attribute if value is empty (not null)
if (attributeValue == string.Empty) {
// Only if the attribute exists
if (attribute != null) {
node.Attributes.Remove(attribute);
}
} else {
// Create the attribue if does not exists
if (attribute == null) {
// Create the attribute
attribute = node.OwnerDocument.CreateAttribute(attributeName);
// Add it to the current node
node.Attributes.Append(attribute);
}
// Update the attribute value
if (attributeValue != null) {
node.Attributes[attributeName].Value = attributeValue;
}
}
} else {
// nodeName
// x:nodeName
// nodeName[find criteria]
// x:nodeName[find criteria]
// Look for the node (with or without filter criteria)
XmlNode subNode = node.SelectSingleNode(subPath, NameSpaceManager);
// Check if the node does not exists
if (subNode == null) {
string nodeName;
string nodePrefix;
string[] nameSplit = subPath.Split(':');
string nameSpaceUri;
// Check if the name has a prefix like "d:nodeName"
if (nameSplit.Length > 1) {
nodePrefix = nameSplit[0];
nameSpaceUri = NameSpaceManager.LookupNamespace(nodePrefix);
nodeName = nameSplit[1];
} else {
nodePrefix = string.Empty;
nameSpaceUri = string.Empty;
nodeName = nameSplit[0];
}
// Check if we have a criteria part in the node name
if (nodeName.IndexOf("[") > 0) {
// remove the criteria from the node name
nodeName = nodeName.Substring(0, nodeName.IndexOf("["));
}
if (nodePrefix == string.Empty) {
subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri);
} else {
if (node.OwnerDocument != null
&& node.OwnerDocument.DocumentElement != null
&& node.OwnerDocument.DocumentElement.NamespaceURI == nameSpaceUri
&& node.OwnerDocument.DocumentElement.Prefix == string.Empty) {
subNode = node.OwnerDocument.CreateElement(nodeName, nameSpaceUri);
} else {
subNode = node.OwnerDocument.CreateElement(nodePrefix, nodeName, nameSpaceUri);
}
}
// Check if we need to use the "SchemaOrder"
if (nodeInsertOrder == eNodeInsertOrder.SchemaOrder) {
// Check if the Schema Order List is empty
if (SchemaNodeOrder.Length == 0) {
// Use the "Insert Last" option when Schema Order List is empty
nodeInsertOrder = eNodeInsertOrder.Last;
} else {
// Find the prepend node in order to insert
referenceNode = GetPrependNode(nodeName, node);
if (referenceNode != null) {
nodeInsertOrder = eNodeInsertOrder.Before;
} else {
nodeInsertOrder = eNodeInsertOrder.Last;
}
}
}
switch (nodeInsertOrder) {
case eNodeInsertOrder.After:
node.InsertAfter(subNode, referenceNode);
referenceNode = null;
break;
case eNodeInsertOrder.Before:
node.InsertBefore(subNode, referenceNode);
referenceNode = null;
break;
case eNodeInsertOrder.First:
node.PrependChild(subNode);
break;
case eNodeInsertOrder.Last:
node.AppendChild(subNode);
break;
}
}
// Make the newly created node the top node when the rest of the path
// is being evaluated. So newly created nodes will be the children of the
// one we just created.
node = subNode;
}
}
}
// Return the last created/found node
return node;
}
/// <summary>
/// return Prepend node
/// </summary>
/// <param name="nodeName">name of the node to check</param>
/// <param name="node">Topnode to check children</param>
/// <returns></returns>
private XmlNode GetPrependNode(string nodeName, XmlNode node) {
int pos = GetNodePos(nodeName);
if (pos < 0) {
return null;
}
XmlNode prependNode = null;
foreach (XmlNode childNode in node.ChildNodes) {
int childPos = GetNodePos(childNode.Name);
if (childPos
> -1) //Found?
{
if (childPos
> pos) //Position is before
{
prependNode = childNode;
break;
}
}
}
return prependNode;
}
private int GetNodePos(string nodeName) {
int ix = nodeName.IndexOf(":");
if (ix > 0) {
nodeName = nodeName.Substring(ix + 1, nodeName.Length - (ix + 1));
}
for (int i = 0; i < SchemaNodeOrder.Length; i++) {
if (nodeName == SchemaNodeOrder[i]) {
return i;
}
}
return -1;
}
internal void DeleteAllNode(string path) {
string[] split = path.Split('/');
XmlNode node = TopNode;
foreach (string s in split) {
node = node.SelectSingleNode(s, NameSpaceManager);
if (node != null) {
if (node is XmlAttribute attribute) {
attribute.OwnerElement.Attributes.Remove(attribute);
} else {
node.ParentNode.RemoveChild(node);
}
} else {
break;
}
}
}
internal void DeleteNode(string path) {
var node = TopNode.SelectSingleNode(path, NameSpaceManager);
if (node != null) {
if (node is XmlAttribute att) {
att.OwnerElement.Attributes.Remove(att);
} else {
node.ParentNode.RemoveChild(node);
}
}
}
internal void SetXmlNodeString(string path, string value) {
SetXmlNodeString(TopNode, path, value, false, false);
}
internal void SetXmlNodeString(string path, string value, bool removeIfBlank) {
SetXmlNodeString(TopNode, path, value, removeIfBlank, false);
}
internal void SetXmlNodeString(XmlNode node, string path, string value, bool removeIfBlank) {
SetXmlNodeString(node, path, value, removeIfBlank, false);
}
internal void SetXmlNodeString(
XmlNode node,
string path,
string value,
bool removeIfBlank,
bool insertFirst) {
if (node == null) {
return;
}
if (value == "" && removeIfBlank) {
DeleteAllNode(path);
} else {
XmlNode nameNode = node.SelectSingleNode(path, NameSpaceManager);
if (nameNode == null) {
CreateNode(path, insertFirst);
nameNode = node.SelectSingleNode(path, NameSpaceManager);
}
//if (nameNode.InnerText != value) HasChanged();
nameNode.InnerText = value;
}
}
internal void SetXmlNodeBool(string path, bool value) {
SetXmlNodeString(TopNode, path, value ? "1" : "0", false, false);
}
internal void SetXmlNodeBool(string path, bool value, bool removeIf) {
if (value == removeIf) {
var node = TopNode.SelectSingleNode(path, NameSpaceManager);
if (node != null) {
if (node is XmlAttribute attribute) {
var elem = attribute.OwnerElement;
elem.ParentNode.RemoveChild(elem);
} else {
TopNode.RemoveChild(node);
}
}
} else {
SetXmlNodeString(TopNode, path, value ? "1" : "0", false, false);
}
}
internal bool ExistNode(string path) {
if (TopNode == null || TopNode.SelectSingleNode(path, NameSpaceManager) == null) {
return false;
}
return true;
}
internal bool? GetXmlNodeBoolNullable(string path) {
var value = GetXmlNodeString(path);
if (string.IsNullOrEmpty(value)) {
return null;
}
return GetXmlNodeBool(path);
}
internal bool GetXmlNodeBool(string path) {
return GetXmlNodeBool(path, false);
}
internal bool GetXmlNodeBool(string path, bool blankValue) {
string value = GetXmlNodeString(path);
if (value == "1"
|| value == "-1"
|| value.Equals("true", StringComparison.InvariantCultureIgnoreCase)) {
return true;
}
if (value == "") {
return blankValue;
}
return false;
}
internal int GetXmlNodeInt(string path) {
if (int.TryParse(GetXmlNodeString(path), out var i)) {
return i;
}
return int.MinValue;
}
internal int? GetXmlNodeIntNull(string path) {
string s = GetXmlNodeString(path);
if (s != "" && int.TryParse(s, out var i)) {
return i;
}
return null;
}
internal decimal GetXmlNodeDecimal(string path) {
if (decimal.TryParse(
GetXmlNodeString(path),
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var d)) {
return d;
}
return 0;
}
internal decimal? GetXmlNodeDecimalNull(string path) {
if (decimal.TryParse(
GetXmlNodeString(path),
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var d)) {
return d;
}
return null;
}
internal double? GetXmlNodeDoubleNull(string path) {
string s = GetXmlNodeString(path);
if (s == "") {
return null;
}
if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var v)) {
return v;
}
return null;
}
internal double GetXmlNodeDouble(string path) {
string s = GetXmlNodeString(path);
if (s == "") {
return double.NaN;
}
if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var v)) {
return v;
}
return double.NaN;
}
internal string GetXmlNodeString(XmlNode node, string path) {
if (node == null) {
return "";
}
XmlNode nameNode = node.SelectSingleNode(path, NameSpaceManager);
if (nameNode != null) {
if (nameNode.NodeType == XmlNodeType.Attribute) {
return nameNode.Value ?? "";
}
return nameNode.InnerText;
}
return "";
}
internal string GetXmlNodeString(string path) {
return GetXmlNodeString(TopNode, path);
}
internal static void LoadXmlSafe(XmlDocument xmlDoc, Stream stream) {
XmlReaderSettings settings = new XmlReaderSettings();
//Disable entity parsing (to aviod xmlbombs, External Entity Attacks etc).
settings.ProhibitDtd = true;
XmlReader reader = XmlReader.Create(stream, settings);
xmlDoc.Load(reader);
}
internal static void LoadXmlSafe(XmlDocument xmlDoc, string xml, Encoding encoding) {
var stream = new MemoryStream(encoding.GetBytes(xml));
LoadXmlSafe(xmlDoc, stream);
}
}