using System; using System.Collections.Generic; using System.Linq; using System.Data; using System.Xml; using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.propertytype; using umbraco.DataLayer; using System.Runtime.CompilerServices; using umbraco.cms.helpers; using umbraco.BusinessLogic; using umbraco.interfaces; using System.IO; using umbraco.IO; using umbraco.cms.businesslogic.datatype.controls; namespace umbraco.cms.businesslogic { /// /// Content is an intermediate layer between CMSNode and class'es which will use generic data. /// /// Content is a datastructure that holds generic data defined in its corresponding ContentType. Content can in some /// sence be compared to a row in a database table, it's contenttype hold a definition of the columns and the Content /// contains the data /// /// Note that Content data in umbraco is *not* tablular but in a treestructure. /// /// public class Content : CMSNode { #region Private Members private Guid _version; private DateTime _versionDate; private XmlNode _xml; private bool _versionDateInitialized; private string _contentTypeIcon; private ContentType _contentType; private Properties m_LoadedProperties = null; #endregion #region Constructors public Content(int id) : base(id) { } protected Content(int id, bool noSetup) : base(id, noSetup) { } protected Content(Guid id) : base(id) { } protected Content(Guid id, bool noSetup) : base(id, noSetup) { } #endregion #region Static Methods /// /// Retrive a list of Content sharing the ContentType /// /// The ContentType /// A list of Content objects sharing the ContentType defined. public static Content[] getContentOfContentType(ContentType ct) { IRecordsReader dr = SqlHelper.ExecuteReader("Select nodeId from cmsContent INNER JOIN umbracoNode ON cmsContent.nodeId = umbracoNode.id where ContentType = " + ct.Id + " ORDER BY umbracoNode.text "); System.Collections.ArrayList tmp = new System.Collections.ArrayList(); while (dr.Read()) tmp.Add(dr.GetInt("nodeId")); dr.Close(); Content[] retval = new Content[tmp.Count]; for (int i = 0; i < tmp.Count; i++) retval[i] = new Content((int)tmp[i]); return retval; } /// /// Initialize a contentobject given a version. /// /// The version identifier /// The Content object from the given version public static Content GetContentFromVersion(Guid version) { int tmpContentId = SqlHelper.ExecuteScalar("Select ContentId from cmsContentVersion where versionId = '" + version.ToString() + "'"); return new Content(tmpContentId); } #endregion #region Public Properties /// /// The current Content objects ContentType, which defines the Properties of the Content (data) /// public ContentType ContentType { get { if (_contentType == null) { object o = SqlHelper.ExecuteScalar( "Select ContentType from cmsContent where nodeId=@nodeid", SqlHelper.CreateParameter("@nodeid", this.Id)); if (o == null) return null; int contentTypeId; if (!int.TryParse(o.ToString(), out contentTypeId)) return null; try { _contentType = new ContentType(contentTypeId); } catch { return null; } } return _contentType; } set { _contentType = value; } } /// /// The icon used in the tree - placed in this layer for performance reasons. /// /// /// This is here for performance reasons only. If the _contentTypeIcon is manually set /// then a database call is not made to initialize the ContentType. /// /// The data layer has slightly changed in 4.1 so that for Document and Media, the ContentType /// is automatically initialized with one SQL call when creating the documents/medias so using this /// method or the ContentType.IconUrl property when accessing the icon from Media or Document /// won't affect performance. /// public string ContentTypeIcon { get { if (_contentTypeIcon == null && this.ContentType != null) _contentTypeIcon = this.ContentType.IconUrl; return _contentTypeIcon; } set { _contentTypeIcon = value; } } /// /// The createtimestamp on this version /// public DateTime VersionDate { get { if (!_versionDateInitialized) { object o = SqlHelper.ExecuteScalar( "select VersionDate from cmsContentVersion where versionId = '" + this.Version.ToString() + "'"); if (o == null) { _versionDate = DateTime.Now; } else { _versionDateInitialized = DateTime.TryParse(o.ToString(), out _versionDate); } } return _versionDate; } set { _versionDate = value; _versionDateInitialized = true; } } /// /// Retrieve a list of generic properties of the content /// public Properties GenericProperties { get { EnsureProperties(); return m_LoadedProperties; } } /// /// Retrieve a list of generic properties of the content /// [Obsolete("Use the GenericProperties property instead")] public Property[] getProperties { get { EnsureProperties(); return m_LoadedProperties.ToArray(); //if (this.ContentType == null) // return new Property[0]; //List result = new List(); //foreach (PropertyType prop in this.ContentType.PropertyTypes) //{ // if (prop == null) // continue; // Property p = getProperty(prop); // if (p == null) // continue; // result.Add(p); //} //return result.ToArray(); } } /// /// Content is under version control, you are able to programatically create new versions /// public Guid Version { get { if (_version == Guid.Empty) { string sql = "Select versionId from cmsContentVersion where contentID = " + this.Id + " order by id desc "; using (IRecordsReader dr = SqlHelper.ExecuteReader(sql)) { if (!dr.Read()) _version = Guid.Empty; else _version = dr.GetGuid("versionId"); } } return _version; } set { _version = value; } } #endregion #region Public Methods /// /// Used to persist object changes to the database. This ensures that the properties are re-loaded from the database. /// public override void Save() { base.Save(); ClearLoadedProperties(); } /// /// Retrieve a Property given the alias /// /// Propertyalias (defined in the documenttype) /// The property with the given alias public Property getProperty(string alias) { ContentType ct = this.ContentType; if (ct == null) return null; propertytype.PropertyType pt = ct.getPropertyType(alias); if (pt == null) return null; return getProperty(pt); } /// /// Retrieve a property given the propertytype /// /// PropertyType /// The property with the given propertytype public Property getProperty(PropertyType pt) { //object o = SqlHelper.ExecuteScalar( // "select id from cmsPropertyData where versionId=@version and propertyTypeId=@propertyTypeId", // SqlHelper.CreateParameter("@version", this.Version), // SqlHelper.CreateParameter("@propertyTypeId", pt.Id)); //if (o == null) // return null; //int propertyId; //if (!int.TryParse(o.ToString(), out propertyId)) // return null; //try //{ // return new Property(propertyId, pt); //} //catch (Exception ex) //{ // Log.Add(LogTypes.Error, this.Id, "An error occurred retreiving property. EXCEPTION: " + ex.Message); // return null; //} EnsureProperties(); var prop = m_LoadedProperties .Where(x => x.PropertyType.Id == pt.Id) .SingleOrDefault(); return prop; } /// /// Add a property to the Content /// /// The PropertyType of the Property /// The version of the document on which the property should be add'ed /// The new Property public Property addProperty(PropertyType pt, Guid versionId) { ClearLoadedProperties(); return property.Property.MakeNew(pt, this, versionId); } /// /// An Xmlrepresentation of a Content object. /// /// Xmldocument context /// If true, the Contents children are appended to the Xmlnode recursive /// The Xmlrepresentation of the data on the Content object public override XmlNode ToXml(XmlDocument xd, bool Deep) { if (_xml == null) { XmlDocument xmlDoc = new XmlDocument(); // we add a try/catch clause here, as the xmlreader will throw an exception if there's no xml in the table // after the empty catch we'll generate the xml which is why we don't do anything in the catch part try { XmlReader xr = SqlHelper.ExecuteXmlReader("select xml from cmsContentXml where nodeID = " + this.Id.ToString()); if (xr.MoveToContent() != System.Xml.XmlNodeType.None) { xmlDoc.Load(xr); _xml = xmlDoc.FirstChild; } xr.Close(); } catch { } // Generate xml if xml still null (then it hasn't been initialized before) if (_xml == null) { this.XmlGenerate(new XmlDocument()); _xml = importXml(); } } XmlNode x = xd.ImportNode(_xml, true); if (Deep) { var childs = this.Children; foreach (BusinessLogic.console.IconI c in childs) { try { x.AppendChild(new Content(c.Id).ToXml(xd, true)); } catch (Exception mExp) { System.Web.HttpContext.Current.Trace.Warn("Content", "Error adding node to xml: " + mExp.ToString()); } } } return x; } /// /// Removes the Xml cached in the database - unpublish and cleaning /// public virtual void XmlRemoveFromDB() { SqlHelper.ExecuteNonQuery("delete from cmsContentXml where nodeId = @nodeId", SqlHelper.CreateParameter("@nodeId", this.Id)); } /// /// Generates the Content XmlNode /// /// public virtual void XmlGenerate(XmlDocument xd) { XmlNode node = generateXmlWithoutSaving(xd); SaveXmlDocument(node); } public virtual void XmlPopulate(XmlDocument xd, ref XmlNode x, bool Deep) { var props = this.getProperties; foreach (property.Property p in props) if (p != null) x.AppendChild(p.ToXml(xd)); // attributes x.Attributes.Append(xmlHelper.addAttribute(xd, "id", this.Id.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "version", this.Version.ToString())); if (this.Level > 1) x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.Parent.Id.ToString())); else x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", "-1")); x.Attributes.Append(xmlHelper.addAttribute(xd, "level", this.Level.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "writerID", this.User.Id.ToString())); if (this.ContentType != null) x.Attributes.Append(xmlHelper.addAttribute(xd, "nodeType", this.ContentType.Id.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "template", "0")); x.Attributes.Append(xmlHelper.addAttribute(xd, "sortOrder", this.sortOrder.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "createDate", this.CreateDateTime.ToString("s"))); x.Attributes.Append(xmlHelper.addAttribute(xd, "updateDate", this.VersionDate.ToString("s"))); x.Attributes.Append(xmlHelper.addAttribute(xd, "nodeName", this.Text)); if (this.Text != null) x.Attributes.Append(xmlHelper.addAttribute(xd, "urlName", this.Text.Replace(" ", "").ToLower())); x.Attributes.Append(xmlHelper.addAttribute(xd, "writerName", this.User.Name)); if (this.ContentType != null) x.Attributes.Append(xmlHelper.addAttribute(xd, "nodeTypeAlias", this.ContentType.Alias)); x.Attributes.Append(xmlHelper.addAttribute(xd, "path", this.Path)); if (Deep) { //store children array here because iterating over an Array property object is very inneficient. var children = this.Children; foreach (Content c in children) x.AppendChild(c.ToXml(xd, true)); } } /// /// Deletes the current Content object, must be overridden in the child class. /// public override void delete() { ClearLoadedProperties(); // Delete all data associated with this content this.deleteAllProperties(); // Remove all content preview xml SqlHelper.ExecuteNonQuery("delete from cmsPreviewXml where nodeId = " + Id); // Delete version history SqlHelper.ExecuteNonQuery("Delete from cmsContentVersion where ContentId = " + this.Id); // Delete xml SqlHelper.ExecuteNonQuery("delete from cmsContentXml where nodeID = @nodeId", SqlHelper.CreateParameter("@nodeId", this.Id)); // Delete Contentspecific data () SqlHelper.ExecuteNonQuery("Delete from cmsContent where NodeId = " + this.Id); // Delete Nodeinformation!! base.delete(); } #endregion #region Protected Methods /// /// Sets up the ContentType property for this content item and sets the addition content properties manually. /// If the ContentType property is not already set, then this will get the ContentType from Cache. /// /// /// /// /// protected void InitializeContent(int InitContentType, Guid InitVersion, DateTime InitVersionDate, string InitContentTypeIcon) { ClearLoadedProperties(); if (_contentType == null) _contentType = ContentType.GetContentType(InitContentType); _version = InitVersion; _versionDate = InitVersionDate; _contentTypeIcon = InitContentTypeIcon; } /// /// Creates a new Content object from the ContentType. /// /// protected void CreateContent(ContentType ct) { SqlHelper.ExecuteNonQuery("insert into cmsContent (nodeId,ContentType) values (" + this.Id + "," + ct.Id + ")"); createNewVersion(); } /// /// Method for creating a new version of the data associated to the Content. /// /// /// The new version Id protected Guid createNewVersion() { ClearLoadedProperties(); Guid newVersion = Guid.NewGuid(); bool tempHasVersion = hasVersion(); foreach (propertytype.PropertyType pt in this.ContentType.PropertyTypes) { object oldValue = ""; if (tempHasVersion) { try { oldValue = this.getProperty(pt.Alias).Value; } catch { } } property.Property p = this.addProperty(pt, newVersion); if (oldValue != null && oldValue.ToString() != "") p.Value = oldValue; } SqlHelper.ExecuteNonQuery("Insert into cmsContentVersion (ContentId,versionId) values (" + this.Id + ",'" + newVersion + "')"); this.Version = newVersion; return newVersion; } protected virtual XmlNode generateXmlWithoutSaving(XmlDocument xd) { string nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : Casing.SafeAliasWithForcingCheck(ContentType.Alias); XmlNode x = xd.CreateNode(XmlNodeType.Element, nodeName, ""); XmlPopulate(xd, ref x, false); return x; } /// /// Saves the XML document to the data source. /// /// The XML Document. [MethodImpl(MethodImplOptions.Synchronized)] protected virtual void SaveXmlDocument(XmlNode node) { // Method is synchronized so exists remains consistent (avoiding race condition) bool exists = SqlHelper.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @nodeId", SqlHelper.CreateParameter("@nodeId", Id)) > 0; string query; if (exists) query = "UPDATE cmsContentXml SET xml = @xml WHERE nodeId = @nodeId"; else query = "INSERT INTO cmsContentXml(nodeId, xml) VALUES (@nodeId, @xml)"; SqlHelper.ExecuteNonQuery(query, SqlHelper.CreateParameter("@nodeId", Id), SqlHelper.CreateParameter("@xml", node.OuterXml)); } /// /// Deletes all files and the folder that have been saved with this content item which are based on the Upload data /// type. This is called when a media or content tree node is deleted. /// protected void DeleteAssociatedMediaFiles() { // Remove all files IDataType uploadField = new Factory().GetNewObject(new Guid("5032a6e6-69e3-491d-bb28-cd31cd11086c")); foreach (Property p in GenericProperties) { var isUploadField = false; try { if (p.PropertyType.DataTypeDefinition.DataType.Id == uploadField.Id && p.Value.ToString() != "" && File.Exists(IOHelper.MapPath(p.Value.ToString()))) { isUploadField = true; } } catch (ArgumentException) { //the data type definition may not exist anymore at this point because another thread may //have deleted it. isUploadField = false; } if (isUploadField) { var fi = new FileInfo(IOHelper.MapPath(p.Value.ToString())); fi.Directory.GetFiles().ToList().ForEach(x => { x.Delete(); }); fi.Directory.Delete(true); } } } #endregion #region Private Methods /// /// Clears the locally loaded properties which forces them to be reloaded next time they requested /// private void ClearLoadedProperties() { m_LoadedProperties = null; } /// /// Makes sure that the properties are initialized. If they are already initialized, this does nothing. /// private void EnsureProperties() { if (m_LoadedProperties == null) { InitializeProperties(); } } /// /// Loads all properties from database into objects. If this method is re-called, it will re-query the database. /// /// /// This optimizes sql calls. This will first check if all of the properties have been loaded. If not, /// then it will query for all property types for the current version from the db. It will then iterate over each /// cmdPropertyData row and store the id and propertyTypeId in a list for lookup later. Once the records have been /// read, we iterate over the cached property types for this ContentType and create a new property based on /// our stored list of proeprtyTypeIds. We then have a cached list of Property objects which will get returned /// on all subsequent calls and is also used to return a property with calls to getProperty. /// private void InitializeProperties() { m_LoadedProperties = new Properties(); if (this.ContentType == null) return; //Create anonymous typed list with 2 props, Id and PropertyTypeId of type Int. //This will still be an empty list since the props list is empty. var propData = m_LoadedProperties.Select(x => new { Id = 0, PropertyTypeId = 0 }).ToList(); string sql = @"select id, propertyTypeId from cmsPropertyData where versionId=@versionId"; using (IRecordsReader dr = SqlHelper.ExecuteReader(sql, SqlHelper.CreateParameter("@versionId", Version))) { while (dr.Read()) { //add the item to our list propData.Add(new { Id = dr.Get("id"), PropertyTypeId = dr.Get("propertyTypeId") }); } } foreach (PropertyType pt in this.ContentType.PropertyTypes) { if (pt == null) continue; //get the propertyId var property = propData .Where(x => x.PropertyTypeId == pt.Id) .SingleOrDefault(); if (property == null) continue; var propertyId = property.Id; Property p = null; try { p = new Property(propertyId, pt); } catch { continue; //this remains from old code... not sure why we would do this? } m_LoadedProperties.Add(p); } } /// /// Optimized method for bulk deletion of properties´on a Content object. /// protected void deleteAllProperties() { SqlHelper.ExecuteNonQuery("Delete from cmsPropertyData where contentNodeId = @nodeId", SqlHelper.CreateParameter("@nodeId", this.Id)); } private XmlNode importXml() { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(SqlHelper.ExecuteXmlReader("select xml from cmsContentXml where nodeID = " + this.Id.ToString())); return xmlDoc.FirstChild; } /// /// Indication if the Content exists in at least one version. /// /// Returns true if the Content has a version private bool hasVersion() { int versionCount = SqlHelper.ExecuteScalar("select Count(Id) as tmp from cmsContentVersion where contentId = " + this.Id.ToString()); return (versionCount > 0); } #endregion #region XmlPreivew public override XmlNode ToPreviewXml(XmlDocument xd) { if (!PreviewExists(Version)) { saveXmlPreview(xd); } return GetPreviewXml(xd, Version); } private void saveXmlPreview(XmlDocument xd) { SavePreviewXml(generateXmlWithoutSaving(xd), Version); } #endregion } }