using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.IO; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Core.Models { public static class ContentExtensions { #region IContent /// /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) /// /// /// /// /// This is helpful for determining if the published event will execute during the saved event for a content item. /// internal static bool JustPublished(this IContent entity) { var dirty = (IRememberBeingDirty)entity; return dirty.WasPropertyDirty("Published") && entity.Published; } /// /// Determines if the item should be persisted at all /// /// /// /// /// In one particular case, a content item shouldn't be persisted: /// * The item exists and is published /// * A call to ContentService.Save is made /// * The item has not been modified whatsoever apart from changing it's published status from published to saved /// /// In this case, there is no reason to make any database changes at all /// internal static bool RequiresSaving(this IContent entity) { var publishedState = ((Content)entity).PublishedState; return RequiresSaving(entity, publishedState); } /// /// Determines if the item should be persisted at all /// /// /// /// /// /// In one particular case, a content item shouldn't be persisted: /// * The item exists and is published /// * A call to ContentService.Save is made /// * The item has not been modified whatsoever apart from changing it's published status from published to saved /// /// In this case, there is no reason to make any database changes at all /// internal static bool RequiresSaving(this IContent entity, PublishedState publishedState) { var publishedChanged = entity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished; //check if any user prop has changed var propertyValueChanged = entity.IsAnyUserPropertyDirty(); //We need to know if any other property apart from Published was changed here //don't create a new version if the published state has changed to 'Save' but no data has actually been changed if (publishedChanged && entity.Published == false && propertyValueChanged == false) { //at this point we need to check if any non property value has changed that wasn't the published state var changedProps = ((TracksChangesEntityBase)entity).GetDirtyProperties(); if (changedProps.Any(x => x != "Published") == false) { return false; } } return true; } /// /// Determines if a new version should be created /// /// /// /// /// A new version needs to be created when: /// * The publish status is changed /// * The language is changed /// * The item is already published and is being published again and any property value is changed (to enable a rollback) /// internal static bool ShouldCreateNewVersion(this IContent entity) { var publishedState = ((Content)entity).PublishedState; return ShouldCreateNewVersion(entity, publishedState); } /// /// Returns a list of all dirty user defined properties /// /// public static IEnumerable GetDirtyUserProperties(this IContentBase entity) { return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); } public static bool IsAnyUserPropertyDirty(this IContentBase entity) { return entity.Properties.Any(x => x.IsDirty()); } public static bool WasAnyUserPropertyDirty(this IContentBase entity) { return entity.Properties.Any(x => x.WasDirty()); } /// /// Determines if a new version should be created /// /// /// /// /// /// A new version needs to be created when: /// * The publish status is changed /// * The language is changed /// * The item is already published and is being published again and any property value is changed (to enable a rollback) /// internal static bool ShouldCreateNewVersion(this IContent entity, PublishedState publishedState) { //check if the published state has changed or the language var publishedChanged = entity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished; var langChanged = entity.IsPropertyDirty("Language"); var contentChanged = publishedChanged || langChanged; //check if any user prop has changed var propertyValueChanged = entity.IsAnyUserPropertyDirty(); //return true if published or language has changed if (contentChanged) { return true; } //check if any content prop has changed var contentDataChanged = ((Content)entity).IsEntityDirty(); //return true if the item is published and a property has changed or if any content property has changed return (propertyValueChanged && publishedState == PublishedState.Published) || contentDataChanged; } /// /// Determines if the published db flag should be set to true for the current entity version and all other db /// versions should have their flag set to false. /// /// /// /// /// This is determined by: /// * If a new version is being created and the entity is published /// * If the published state has changed and the entity is published OR the entity has been un-published. /// internal static bool ShouldClearPublishedFlagForPreviousVersions(this IContent entity) { var publishedState = ((Content)entity).PublishedState; return entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, entity.ShouldCreateNewVersion(publishedState)); } /// /// Determines if the published db flag should be set to true for the current entity version and all other db /// versions should have their flag set to false. /// /// /// /// /// /// /// This is determined by: /// * If a new version is being created and the entity is published /// * If the published state has changed and the entity is published OR the entity has been un-published. /// internal static bool ShouldClearPublishedFlagForPreviousVersions(this IContent entity, PublishedState publishedState, bool isCreatingNewVersion) { if (isCreatingNewVersion && entity.Published) { return true; } //If Published state has changed then previous versions should have their publish state reset. //If state has been changed to unpublished the previous versions publish state should also be reset. if (entity.IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) { return true; } return false; } /// /// Returns a list of the current contents ancestors, not including the content itself. /// /// Current content /// An enumerable list of objects public static IEnumerable Ancestors(this IContent content) { return ApplicationContext.Current.Services.ContentService.GetAncestors(content); } /// /// Returns a list of the current contents children. /// /// Current content /// An enumerable list of objects public static IEnumerable Children(this IContent content) { return ApplicationContext.Current.Services.ContentService.GetChildren(content.Id); } /// /// Returns a list of the current contents descendants, not including the content itself. /// /// Current content /// An enumerable list of objects public static IEnumerable Descendants(this IContent content) { return ApplicationContext.Current.Services.ContentService.GetDescendants(content); } /// /// Returns the parent of the current content. /// /// Current content /// An object public static IContent Parent(this IContent content) { return ApplicationContext.Current.Services.ContentService.GetById(content.ParentId); } #endregion #region IMedia /// /// Returns a list of the current medias ancestors, not including the media itself. /// /// Current media /// An enumerable list of objects public static IEnumerable Ancestors(this IMedia media) { return ApplicationContext.Current.Services.MediaService.GetAncestors(media); } /// /// Returns a list of the current medias children. /// /// Current media /// An enumerable list of objects public static IEnumerable Children(this IMedia media) { return ApplicationContext.Current.Services.MediaService.GetChildren(media.Id); } /// /// Returns a list of the current medias descendants, not including the media itself. /// /// Current media /// An enumerable list of objects public static IEnumerable Descendants(this IMedia media) { return ApplicationContext.Current.Services.MediaService.GetDescendants(media); } /// /// Returns the parent of the current media. /// /// Current media /// An object public static IMedia Parent(this IMedia media) { return ApplicationContext.Current.Services.MediaService.GetById(media.ParentId); } #endregion internal static bool IsInRecycleBin(this IContent content) { return IsInRecycleBin(content, Constants.System.RecycleBinContent); } internal static bool IsInRecycleBin(this IMedia media) { return IsInRecycleBin(media, Constants.System.RecycleBinMedia); } internal static bool IsInRecycleBin(this IContentBase content, int recycleBinId) { return content.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Contains(recycleBinId.ToInvariantString()); } /// /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 /// /// /// /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. /// /// public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) { entity.Name = entity.Name.ToValidXmlString(); foreach (var property in entity.Properties) { if (property.Value is string) { var value = (string) property.Value; property.Value = value.ToValidXmlString(); } } } /// /// Checks if the IContentBase has children /// /// /// /// /// /// This is a bit of a hack because we need to type check! /// internal static bool HasChildren(IContentBase content, ServiceContext services) { if (content is IContent) { return services.ContentService.HasChildren(content.Id); } if (content is IMedia) { return services.MediaService.HasChildren(content.Id); } return false; } /// /// Returns the children for the content base item /// /// /// /// /// /// This is a bit of a hack because we need to type check! /// internal static IEnumerable Children(IContentBase content, ServiceContext services) { if (content is IContent) { return services.ContentService.GetChildren(content.Id); } if (content is IMedia) { return services.MediaService.GetChildren(content.Id); } return null; } /// /// Returns properties that do not belong to a group /// /// /// public static IEnumerable GetNonGroupedProperties(this IContentBase content) { var propertyIdsInTabs = content.PropertyGroups.SelectMany(pg => pg.PropertyTypes); return content.Properties .Where(property => propertyIdsInTabs.Contains(property.PropertyType) == false) .OrderBy(x => x.PropertyType.SortOrder); } /// /// Returns the Property object for the given property group /// /// /// /// public static IEnumerable GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) { //get the properties for the current tab return content.Properties .Where(property => propertyGroup.PropertyTypes .Select(propertyType => propertyType.Id) .Contains(property.PropertyTypeId)); } /// /// Set property values by alias with an annonymous object /// public static void PropertyValues(this IContentBase content, object value) { if (value == null) throw new Exception("No properties has been passed in"); var propertyInfos = value.GetType().GetProperties(); foreach (var propertyInfo in propertyInfos) { //Check if a PropertyType with alias exists thus being a valid property var propertyType = content.PropertyTypes.FirstOrDefault(x => x.Alias == propertyInfo.Name); if (propertyType == null) throw new Exception( string.Format( "The property alias {0} is not valid, because no PropertyType with this alias exists", propertyInfo.Name)); //Check if a Property with the alias already exists in the collection thus being updated or inserted var item = content.Properties.FirstOrDefault(x => x.Alias == propertyInfo.Name); if (item != null) { item.Value = propertyInfo.GetValue(value, null); //Update item with newly added value content.Properties.Add(item); } else { //Create new Property to add to collection var property = propertyType.CreatePropertyFromValue(propertyInfo.GetValue(value, null)); content.Properties.Add(property); } } } public static IContentTypeComposition GetContentType(this IContentBase contentBase) { if (contentBase == null) throw new ArgumentNullException("contentBase"); var content = contentBase as IContent; if (content != null) return content.ContentType; var media = contentBase as IMedia; if (media != null) return media.ContentType; var member = contentBase as IMember; if (member != null) return member.ContentType; throw new NotSupportedException("Unsupported IContentBase implementation: " + contentBase.GetType().FullName + "."); } #region SetValue for setting file contents /// /// Stores and sets an uploaded HttpPostedFileBase as a property value. /// /// A content item. /// The property alias. /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { // ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie var filename = value.FileName; var pos = filename.LastIndexOf(@"\", StringComparison.InvariantCulture); if (pos > 0) filename = filename.Substring(pos + 1); // strip any directory info pos = filename.LastIndexOf(IOHelper.DirSepChar); if (pos > 0) filename = filename.Substring(pos + 1); // get a safe & clean filename filename = IOHelper.SafeFileName(filename); if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// /// Stores and sets an uploaded HttpPostedFile as a property value. /// /// A content item. /// The property alias. /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFile value) { SetValue(content, propertyTypeAlias, (HttpPostedFileBase) new HttpPostedFileWrapper(value)); } /// /// Stores and sets an uploaded HttpPostedFileWrapper as a property value. /// /// A content item. /// The property alias. /// The uploaded . [Obsolete("There is no reason for this overload since HttpPostedFileWrapper inherits from HttpPostedFileBase")] public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileWrapper value) { SetValue(content, propertyTypeAlias, (HttpPostedFileBase) value); } /// /// Stores and sets a file as a property value. /// /// A content item. /// The property alias. /// The name of the file. /// A stream containing the file data. /// This really is for FileUpload fields only, and should be obsoleted. For anything else, /// you need to store the file by yourself using Store and then figure out /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream) { if (filename == null || filestream == null) return; // get a safe & clean filename filename = IOHelper.SafeFileName(filename); if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream); } /// /// Stores a file. /// /// A content item. /// The property alias. /// The name of the file. /// A stream containing the file data. /// The original file path, if any. /// The path to the file, relative to the media filesystem. /// /// Does NOT set the property value, so one should probably store the file and then do /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). /// The original file path is used, in the old media file path scheme, to try and reuse /// the "folder number" that was assigned to the previous file referenced by the property, /// if any. /// public static string StoreFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string filepath) { var propertyType = content.GetContentType() .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); return FileSystemProviderManager.Current.MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion #region User/Profile methods /// /// Gets the for the Creator of this media item. /// [Obsolete("Use the overload that declares the IUserService to use")] public static IProfile GetCreatorProfile(this IMedia media) { return ApplicationContext.Current.Services.UserService.GetProfileById(media.CreatorId); } /// /// Gets the for the Creator of this media item. /// public static IProfile GetCreatorProfile(this IMedia media, IUserService userService) { return userService.GetProfileById(media.CreatorId); } /// /// Gets the for the Creator of this content item. /// [Obsolete("Use the overload that declares the IUserService to use")] public static IProfile GetCreatorProfile(this IContentBase content) { return ApplicationContext.Current.Services.UserService.GetProfileById(content.CreatorId); } /// /// Gets the for the Creator of this content item. /// public static IProfile GetCreatorProfile(this IContentBase content, IUserService userService) { return userService.GetProfileById(content.CreatorId); } /// /// Gets the for the Writer of this content. /// [Obsolete("Use the overload that declares the IUserService to use")] public static IProfile GetWriterProfile(this IContent content) { return ApplicationContext.Current.Services.UserService.GetProfileById(content.WriterId); } /// /// Gets the for the Writer of this content. /// public static IProfile GetWriterProfile(this IContent content, IUserService userService) { return userService.GetProfileById(content.WriterId); } #endregion /// /// Checks whether an item has any published versions /// /// /// True if the content has any published versiom otherwise False [Obsolete("Use the HasPublishedVersion property.", false)] public static bool HasPublishedVersion(this IContent content) { return content.HasPublishedVersion; } #region Tag methods ///// ///// Returns the tags for the given property ///// ///// ///// ///// ///// ///// ///// The tags returned are only relavent for published content & saved media or members ///// //public static IEnumerable GetTags(this IContentBase content, string propertyTypeAlias, string tagGroup = "default") //{ //} /// /// Sets tags for the property - will add tags to the tags table and set the property value to be the comma delimited value of the tags. /// /// The content item to assign the tags to /// The property alias to assign the tags to /// The tags to assign /// True to replace the tags on the current property with the tags specified or false to merge them with the currently assigned ones /// The group/category to assign the tags, the default value is "default" /// public static void SetTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") { content.SetTags(TagCacheStorageType.Csv, propertyTypeAlias, tags, replaceTags, tagGroup); } /// /// Sets tags for the property - will add tags to the tags table and set the property value to be the comma delimited value of the tags. /// /// The content item to assign the tags to /// The tag storage type in cache (default is csv) /// The property alias to assign the tags to /// The tags to assign /// True to replace the tags on the current property with the tags specified or false to merge them with the currently assigned ones /// The group/category to assign the tags, the default value is "default" /// public static void SetTags(this IContentBase content, TagCacheStorageType storageType, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") { var property = content.Properties[propertyTypeAlias]; if (property == null) { throw new IndexOutOfRangeException("No property exists with name " + propertyTypeAlias); } property.SetTags(storageType, propertyTypeAlias, tags, replaceTags, tagGroup); } internal static void SetTags(this Property property, TagCacheStorageType storageType, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") { if (property == null) throw new ArgumentNullException("property"); var trimmedTags = tags.Select(x => x.Trim()).ToArray(); property.TagSupport.Enable = true; property.TagSupport.Tags = trimmedTags.Select(x => new Tuple(x, tagGroup)); property.TagSupport.Behavior = replaceTags ? PropertyTagBehavior.Replace : PropertyTagBehavior.Merge; //ensure the property value is set to the same thing if (replaceTags) { switch (storageType) { case TagCacheStorageType.Csv: property.Value = string.Join(",", trimmedTags); break; case TagCacheStorageType.Json: //json array property.Value = JsonConvert.SerializeObject(trimmedTags); break; } } else { switch (storageType) { case TagCacheStorageType.Csv: var currTags = property.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()); property.Value = string.Join(",", trimmedTags.Union(currTags)); break; case TagCacheStorageType.Json: var currJson = JsonConvert.DeserializeObject(property.Value.ToString()); //need to append the new ones foreach (var tag in trimmedTags) { currJson.Add(tag); } //json array property.Value = JsonConvert.SerializeObject(currJson); break; } } } /// /// Remove any of the tags specified in the collection from the property if they are currently assigned. /// /// /// /// /// The group/category that the tags are currently assigned to, the default value is "default" public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, string tagGroup = "default") { var property = content.Properties[propertyTypeAlias]; if (property == null) { throw new IndexOutOfRangeException("No property exists with name " + propertyTypeAlias); } var trimmedTags = tags.Select(x => x.Trim()).ToArray(); property.TagSupport.Behavior = PropertyTagBehavior.Remove; property.TagSupport.Enable = true; property.TagSupport.Tags = trimmedTags.Select(x => new Tuple(x, tagGroup)); //set the property value var currTags = property.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()); property.Value = string.Join(",", currTags.Except(trimmedTags)); } #endregion #region XML methods /// /// Creates the full xml representation for the object and all of it's descendants /// /// to generate xml for /// /// Xml representation of the passed in internal static XElement ToDeepXml(this IContent content, IPackagingService packagingService) { return packagingService.Export(content, true, raiseEvents: false); } /// /// Creates the xml representation for the object /// /// to generate xml for /// Xml representation of the passed in [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IContent content) { return ApplicationContext.Current.Services.PackagingService.Export(content, raiseEvents: false); } /// /// Creates the xml representation for the object /// /// to generate xml for /// /// Xml representation of the passed in public static XElement ToXml(this IContent content, IPackagingService packagingService) { return packagingService.Export(content, raiseEvents: false); } /// /// Creates the xml representation for the object /// /// to generate xml for /// Xml representation of the passed in [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IMedia media) { return ApplicationContext.Current.Services.PackagingService.Export(media, raiseEvents: false); } /// /// Creates the xml representation for the object /// /// to generate xml for /// /// Xml representation of the passed in public static XElement ToXml(this IMedia media, IPackagingService packagingService) { return packagingService.Export(media, raiseEvents: false); } /// /// Creates the full xml representation for the object and all of it's descendants /// /// to generate xml for /// /// Xml representation of the passed in internal static XElement ToDeepXml(this IMedia media, IPackagingService packagingService) { return packagingService.Export(media, true, raiseEvents: false); } /// /// Creates the xml representation for the object /// /// to generate xml for /// Boolean indicating whether the xml should be generated for preview /// Xml representation of the passed in [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IContent content, bool isPreview) { //TODO Do a proper implementation of this //If current IContent is published we should get latest unpublished version return content.ToXml(); } /// /// Creates the xml representation for the object /// /// to generate xml for /// /// Boolean indicating whether the xml should be generated for preview /// Xml representation of the passed in public static XElement ToXml(this IContent content, IPackagingService packagingService, bool isPreview) { //TODO Do a proper implementation of this //If current IContent is published we should get latest unpublished version return content.ToXml(packagingService); } /// /// Creates the xml representation for the object /// /// to generate xml for /// Xml representation of the passed in [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IMember member) { return ((PackagingService)(ApplicationContext.Current.Services.PackagingService)).Export(member); } /// /// Creates the xml representation for the object /// /// to generate xml for /// /// Xml representation of the passed in public static XElement ToXml(this IMember member, IPackagingService packagingService) { return ((PackagingService)(packagingService)).Export(member); } #endregion } }