using System;
using System.Collections.Generic;
using System.Data;
using System.Xml;
using umbraco.cms.businesslogic.web;
using umbraco.DataLayer;
using umbraco.BusinessLogic;
using System.IO;
using System.Text.RegularExpressions;
using System.ComponentModel;
using umbraco.IO;
using umbraco.cms.businesslogic.media;
using System.Collections;
using umbraco.cms.businesslogic.task;
using umbraco.cms.businesslogic.workflow;
using umbraco.cms.businesslogic.Tags;
namespace umbraco.cms.businesslogic
{
///
/// CMSNode class serves as the base class for many of the other components in the cms.businesslogic.xx namespaces.
/// Providing the basic hierarchical data structure and properties Text (name), Creator, Createdate, updatedate etc.
/// which are shared by most umbraco objects.
///
/// The child classes are required to implement an identifier (Guid) which is used as the objecttype identifier, for
/// distinguishing the different types of CMSNodes (ex. Documents/Medias/Stylesheets/documenttypes and so forth).
///
public class CMSNode : BusinessLogic.console.IconI
{
#region Private Members
private string _text;
private int _id = 0;
private Guid _uniqueID;
///
/// Private connectionstring
///
protected static readonly string _ConnString = GlobalSettings.DbDSN;
private int _parentid;
private Guid _nodeObjectType;
private int _level;
private string _path;
private bool _hasChildren;
private int _sortOrder;
private int _userId;
private DateTime _createDate;
private bool _hasChildrenInitialized;
private string m_image = "default.png";
private bool? _isTrashed = null;
#endregion
#region Private static
private static readonly string m_DefaultIconCssFile = IOHelper.MapPath(SystemDirectories.Umbraco_client + "/Tree/treeIcons.css");
private static List m_DefaultIconClasses = new List();
private static void initializeIconClasses()
{
StreamReader re = File.OpenText(m_DefaultIconCssFile);
string content = string.Empty;
string input = null;
while ((input = re.ReadLine()) != null)
{
content += input.Replace("\n", "") + "\n";
}
re.Close();
// parse the classes
MatchCollection m = Regex.Matches(content, "([^{]*){([^}]*)}", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
foreach (Match match in m)
{
GroupCollection groups = match.Groups;
string cssClass = groups[1].Value.Replace("\n", "").Replace("\r", "").Trim().Trim(Environment.NewLine.ToCharArray());
if (!String.IsNullOrEmpty(cssClass))
{
m_DefaultIconClasses.Add(cssClass);
}
}
}
private const string m_SQLSingle = "SELECT id, createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text FROM umbracoNode WHERE id = @id";
private const string m_SQLDescendants = @"
SELECT id, createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text
FROM umbracoNode
WHERE path LIKE '%,{0},%'";
#endregion
#region Public static
///
/// Get a count on all CMSNodes given the objecttype
///
/// The objecttype identifier
///
/// The number of CMSNodes of the given objecttype
///
public static int CountByObjectType(Guid objectType)
{
return SqlHelper.ExecuteScalar("SELECT COUNT(*) from umbracoNode WHERE nodeObjectType = @type", SqlHelper.CreateParameter("@type", objectType));
}
///
/// Number of children of the current CMSNode
///
/// The CMSNode Id
///
/// The number of children from the given CMSNode
///
public static int CountSubs(int Id)
{
return SqlHelper.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE ','+path+',' LIKE '%," + Id.ToString() + ",%'");
}
///
/// Gets the default icon classes.
///
/// The default icon classes.
public static List DefaultIconClasses
{
get
{
if (m_DefaultIconClasses.Count == 0)
initializeIconClasses();
return m_DefaultIconClasses;
}
}
///
/// Method for checking if a CMSNode exits with the given Guid
///
/// Identifier
/// True if there is a CMSNode with the given Guid
public static bool IsNode(Guid uniqueID)
{
return (SqlHelper.ExecuteScalar("select count(id) from umbracoNode where uniqueID = @uniqueID", SqlHelper.CreateParameter("@uniqueId", uniqueID)) > 0);
}
///
/// Method for checking if a CMSNode exits with the given id
///
/// Identifier
/// True if there is a CMSNode with the given id
public static bool IsNode(int Id)
{
return (SqlHelper.ExecuteScalar("select count(id) from umbracoNode where id = @id", SqlHelper.CreateParameter("@id", Id)) > 0);
}
///
/// Retrieve a list of the unique id's of all CMSNodes given the objecttype
///
/// The objecttype identifier
///
/// A list of all unique identifiers which each are associated to a CMSNode
///
public static Guid[] getAllUniquesFromObjectType(Guid objectType)
{
IRecordsReader dr = SqlHelper.ExecuteReader("Select uniqueID from umbracoNode where nodeObjectType = @type",
SqlHelper.CreateParameter("@type", objectType));
System.Collections.ArrayList tmp = new System.Collections.ArrayList();
while (dr.Read()) tmp.Add(dr.GetGuid("uniqueID"));
dr.Close();
Guid[] retval = new Guid[tmp.Count];
for (int i = 0; i < tmp.Count; i++) retval[i] = (Guid)tmp[i];
return retval;
}
///
/// Retrieve a list of the node id's of all CMSNodes given the objecttype
///
/// The objecttype identifier
///
/// A list of all node ids which each are associated to a CMSNode
///
public static int[] getAllUniqueNodeIdsFromObjectType(Guid objectType)
{
IRecordsReader dr = SqlHelper.ExecuteReader("Select id from umbracoNode where nodeObjectType = @type",
SqlHelper.CreateParameter("@type", objectType));
System.Collections.ArrayList tmp = new System.Collections.ArrayList();
while (dr.Read()) tmp.Add(dr.GetInt("id"));
dr.Close();
return (int[])tmp.ToArray(typeof(int));
}
#endregion
#region Protected static
///
/// Retrieves the top level nodes in the hierarchy
///
/// The Guid identifier of the type of objects
///
/// A list of all top level nodes given the objecttype
///
protected static Guid[] TopMostNodeIds(Guid ObjectType)
{
IRecordsReader dr = SqlHelper.ExecuteReader("Select uniqueID from umbracoNode where nodeObjectType = @type And parentId = -1 order by sortOrder",
SqlHelper.CreateParameter("@type", ObjectType));
System.Collections.ArrayList tmp = new System.Collections.ArrayList();
while (dr.Read()) tmp.Add(dr.GetGuid("uniqueID"));
dr.Close();
Guid[] retval = new Guid[tmp.Count];
for (int i = 0; i < tmp.Count; i++) retval[i] = (Guid)tmp[i];
return retval;
}
///
/// Given the protected modifier the CMSNode.MakeNew method can only be accessed by
/// derived classes > who by definition knows of its own objectType.
///
/// The parent CMSNode id
/// The objecttype identifier
/// Creator
/// The level in the tree hieararchy
/// The name of the CMSNode
/// The unique identifier
///
protected static CMSNode MakeNew(int parentId, Guid objectType, int userId, int level, string text, Guid uniqueID)
{
CMSNode parent;
string path = "";
int sortOrder = 0;
if (level > 0)
{
parent = new CMSNode(parentId);
sortOrder = parent.ChildCount + 1;
path = parent.Path;
}
else
path = "-1";
// Ruben 8/1/2007: I replace this with a parameterized version.
// But does anyone know what the 'level++' is supposed to be doing there?
// Nothing obviously, since it's a postfix.
SqlHelper.ExecuteNonQuery("INSERT INTO umbracoNode(trashed, parentID, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text, createDate) VALUES(@trashed, @parentID, @nodeObjectType, @nodeUser, @level, @path, @sortOrder, @uniqueID, @text, @createDate)",
SqlHelper.CreateParameter("@trashed", 0),
SqlHelper.CreateParameter("@parentID", parentId),
SqlHelper.CreateParameter("@nodeObjectType", objectType),
SqlHelper.CreateParameter("@nodeUser", userId),
SqlHelper.CreateParameter("@level", level++),
SqlHelper.CreateParameter("@path", path),
SqlHelper.CreateParameter("@sortOrder", sortOrder),
SqlHelper.CreateParameter("@uniqueID", uniqueID),
SqlHelper.CreateParameter("@text", text),
SqlHelper.CreateParameter("@createDate", DateTime.Now));
CMSNode retVal = new CMSNode(uniqueID);
retVal.Path = path + "," + retVal.Id.ToString();
//event
NewEventArgs e = new NewEventArgs();
retVal.FireAfterNew(e);
return retVal;
}
///
/// Retrieve a list of the id's of all CMSNodes given the objecttype and the first letter of the name.
///
/// The objecttype identifier
/// Firstletter
///
/// A list of all CMSNodes which has the objecttype and a name that starts with the given letter
///
protected static int[] getUniquesFromObjectTypeAndFirstLetter(Guid objectType, char letter)
{
using (IRecordsReader dr = SqlHelper.ExecuteReader("Select id from umbracoNode where nodeObjectType = @objectType AND text like @letter", SqlHelper.CreateParameter("@objectType", objectType), SqlHelper.CreateParameter("@letter", letter.ToString() + "%")))
{
List tmp = new List();
while (dr.Read()) tmp.Add(dr.GetInt("id"));
return tmp.ToArray();
}
}
///
/// Gets the SQL helper.
///
/// The SQL helper.
protected static ISqlHelper SqlHelper
{
get { return Application.SqlHelper; }
}
#endregion
#region Constructors
///
/// Empty constructor that is not suported
/// ...why is it here?
///
public CMSNode()
{
throw new NotSupportedException();
}
///
/// Initializes a new instance of the class.
///
/// The id.
public CMSNode(int Id)
{
_id = Id;
setupNode();
}
///
/// Initializes a new instance of the class.
///
/// The id.
/// if set to true [no setup].
public CMSNode(int id, bool noSetup)
{
_id = id;
if (!noSetup)
setupNode();
}
///
/// Initializes a new instance of the class.
///
/// The unique ID.
public CMSNode(Guid uniqueID)
{
_id = SqlHelper.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueID = @uniqueId", SqlHelper.CreateParameter("@uniqueId", uniqueID));
setupNode();
}
public CMSNode(Guid uniqueID, bool noSetup)
{
_id = SqlHelper.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueID = @uniqueId", SqlHelper.CreateParameter("@uniqueId", uniqueID));
if (!noSetup)
setupNode();
}
protected internal CMSNode(IRecordsReader reader)
{
_id = reader.GetInt("id");
PopulateCMSNodeFromReader(reader);
}
#endregion
#region Public Methods
///
/// Ensures uniqueness by id
///
///
///
public override bool Equals(object obj)
{
var l = obj as CMSNode;
if (l != null)
{
return this._id.Equals(l._id);
}
return false;
}
///
/// Ensures uniqueness by id
///
///
public override int GetHashCode()
{
return _id.GetHashCode();
}
///
/// An xml representation of the CMSNOde
///
/// Xmldocument context
/// If true the xml will append the CMSNodes child xml
/// The CMSNode Xmlrepresentation
public virtual XmlNode ToXml(XmlDocument xd, bool Deep)
{
XmlNode x = xd.CreateNode(XmlNodeType.Element, "node", "");
XmlPopulate(xd, x, Deep);
return x;
}
public virtual XmlNode ToPreviewXml(XmlDocument xd)
{
// If xml already exists
if (!PreviewExists(UniqueId))
{
SavePreviewXml(ToXml(xd, false), UniqueId);
}
return GetPreviewXml(xd, UniqueId);
}
public virtual List GetNodesForPreview(bool childrenOnly)
{
List nodes = new List();
string sql = @"
select umbracoNode.id, umbracoNode.parentId, umbracoNode.level, umbracoNode.sortOrder, cmsPreviewXml.xml from umbracoNode
inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id
where trashed = 0 and path like '{0}'
order by level,sortOrder";
string pathExp = childrenOnly ? Path + ",%" : Path;
IRecordsReader dr = SqlHelper.ExecuteReader(String.Format(sql, pathExp));
while (dr.Read())
nodes.Add(new CMSPreviewNode(dr.GetInt("id"), dr.GetGuid("uniqueID"), dr.GetInt("parentId"), dr.GetShort("level"), dr.GetInt("sortOrder"), dr.GetString("xml")));
dr.Close();
return nodes;
}
///
/// Used to persist object changes to the database. In Version3.0 it's just a stub for future compatibility
///
public virtual void Save()
{
SaveEventArgs e = new SaveEventArgs();
this.FireBeforeSave(e);
if (!e.Cancel)
{
//In the future there will be SQL stuff happening here...
this.FireAfterSave(e);
}
}
public override string ToString()
{
if (Id != int.MinValue || !string.IsNullOrEmpty(Text))
{
return string.Format("{{ Id: {0}, Text: {1}, ParentId: {2} }}",
Id,
Text,
_parentid
);
}
return base.ToString();
}
private void Move(CMSNode parent)
{
MoveEventArgs e = new MoveEventArgs();
FireBeforeMove(e);
if (!e.Cancel)
{
//first we need to establish if the node already exists under the parent node
var isSameParent = (Path.Contains("," + parent.Id + ","));
//if it's the same parent, we can save some SQL calls since we know these wont change.
//level and path might change even if it's the same parent because the parent could be moving somewhere.
if (!isSameParent)
{
int maxSortOrder = SqlHelper.ExecuteScalar("select coalesce(max(sortOrder),0) from umbracoNode where parentid = @parentId",
SqlHelper.CreateParameter("@parentId", parent.Id));
this.Parent = parent;
this.sortOrder = maxSortOrder + 1;
}
this.Parent = parent;
this.Level = parent.Level + 1;
this.Path = parent.Path + "," + this.Id.ToString();
//this code block should not be here but since the class structure is very poor and doesn't use
//overrides (instead using shadows/new) for the Children property, when iterating over the children
//and calling Move(), the super classes overridden OnMove or Move methods never get fired, so
//we now need to hard code this here :(
if (Path.Contains("," + ((int)RecycleBin.RecycleBinType.Content).ToString() + ",")
|| Path.Contains("," + ((int)RecycleBin.RecycleBinType.Media).ToString() + ","))
{
//if we've moved this to the recyle bin, we need to update the trashed property
if (!IsTrashed) IsTrashed = true; //don't update if it's not necessary
}
else
{
if (IsTrashed) IsTrashed = false; //don't update if it's not necessary
}
//make sure the node type is a document/media, if it is a recycle bin then this will not be equal
if (!IsTrashed && parent.nodeObjectType == Document._objectType)
{
//regenerate the xml for the parent node
var d = new Document(parent.Id);
d.XmlGenerate(new XmlDocument());
}
else if (!IsTrashed && parent.nodeObjectType == Media._objectType)
{
//regenerate the xml for the parent node
var m = new Media(parent.Id);
m.XmlGenerate(new XmlDocument());
}
var children = this.Children;
foreach (CMSNode c in children)
{
c.Move(this);
}
FireAfterMove(e);
}
}
///
/// Moves the CMSNode from the current position in the hierarchy to the target
///
/// Target CMSNode id
public void Move(int newParentId)
{
CMSNode parent = new CMSNode(newParentId);
Move(parent);
}
///
/// Deletes this instance.
///
public virtual void delete()
{
DeleteEventArgs e = new DeleteEventArgs();
FireBeforeDelete(e);
if (!e.Cancel)
{
// remove relations
var rels = Relations;
foreach (relation.Relation rel in rels)
{
rel.Delete();
}
//removes tasks
foreach (Task t in Tasks)
{
t.Delete();
}
//remove notifications
Notification.DeleteNotifications(this);
//remove permissions
Permission.DeletePermissions(this);
//removes tag associations (i know the key is set to cascade but do it anyways)
Tag.RemoveTagsFromNode(this.Id);
SqlHelper.ExecuteNonQuery("DELETE FROM umbracoNode WHERE uniqueID= @uniqueId", SqlHelper.CreateParameter("@uniqueId", _uniqueID));
FireAfterDelete(e);
}
}
///
/// Does the current CMSNode have any child nodes.
///
///
/// true if this instance has children; otherwise, false.
///
public virtual bool HasChildren
{
get
{
if (!_hasChildrenInitialized)
{
int tmpChildrenCount = SqlHelper.ExecuteScalar("select count(id) from umbracoNode where ParentId = @id",
SqlHelper.CreateParameter("@id", Id));
HasChildren = (tmpChildrenCount > 0);
}
return _hasChildren;
}
set
{
_hasChildrenInitialized = true;
_hasChildren = value;
}
}
///
/// Returns all descendant nodes from this node.
///
///
///
/// This doesn't return a strongly typed IEnumerable object so that we can override in in super clases
/// and since this class isn't a generic (thought it should be) this is not strongly typed.
///
public virtual IEnumerable GetDescendants()
{
var descendants = new List();
using (IRecordsReader dr = SqlHelper.ExecuteReader(string.Format(m_SQLDescendants, Id)))
{
while (dr.Read())
{
var node = new CMSNode(dr.GetInt("id"), true);
node.PopulateCMSNodeFromReader(dr);
descendants.Add(node);
}
}
return descendants;
}
#endregion
#region Public properties
///
/// Determines if the node is in the recycle bin.
/// This is only relavent for node types that support a recyle bin (such as Document/Media)
///
public bool IsTrashed
{
get
{
if (!_isTrashed.HasValue)
{
_isTrashed = Convert.ToBoolean(SqlHelper.ExecuteScalar