using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Web; using System.Xml.Linq; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; namespace Umbraco.Core { public static class ContentExtensions { // this ain't pretty private static MediaFileSystem _mediaFileSystem; private static MediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.FileSystems.MediaFileSystem); #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; } /// /// 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, IContentService contentService) { return 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, IContentService contentService) { return 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, IContentService contentService) { return contentService.GetDescendants(content); } /// /// Returns the parent of the current content. /// /// Current content /// /// An object public static IContent Parent(this IContent content, IContentService contentService) { return 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, IMediaService mediaService) { return 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, IMediaService mediaService) { return 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, IMediaService mediaService) { return mediaService.GetDescendants(media); } /// /// Returns the parent of the current media. /// /// Current media /// /// An object public static IMedia Parent(this IMedia media, IMediaService mediaService) { return mediaService.GetById(media.ParentId); } #endregion /// /// 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) { foreach (var propertyValue in property.Values) { if (propertyValue.EditedValue is string editString) propertyValue.EditedValue = editString.ToValidXmlString(); if (propertyValue.PublishedValue is string publishedString) propertyValue.PublishedValue = publishedString.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)); } public static IContentTypeComposition GetContentType(this IContentBase contentBase) { if (contentBase == null) throw new ArgumentNullException(nameof(contentBase)); if (contentBase is IContent content) return content.ContentType; if (contentBase is IMedia media) return media.ContentType; if (contentBase is IMember member) return member.ContentType; throw new NotSupportedException("Unsupported IContentBase implementation: " + contentBase.GetType().FullName + "."); } #region SetValue for setting file contents /// /// Sets the posted file value of a property. /// /// 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, string culture = null, string segment = null) { if (filename == null || filestream == null) return; // get a safe & clean filename filename = IOHelper.SafeFileName(filename); if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? SetUploadFile(content, propertyTypeAlias, filename, filestream, culture, segment); } private static void SetUploadFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); var oldpath = property.GetValue(culture, segment) is string svalue ? MediaFileSystem.GetRelativePath(svalue) : null; var filepath = MediaFileSystem.StoreFile(content, property.PropertyType, filename, filestream, oldpath); property.SetValue(MediaFileSystem.GetUrl(filepath), culture, segment); } // gets or creates a property for a content item. private static Property GetProperty(IContentBase content, string propertyTypeAlias) { var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (property != null) return property; var propertyType = content.GetContentType().CompositionPropertyTypes .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); property = new Property(propertyType); content.Properties.Add(property); return property; } /// /// 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 MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion #region User/Profile methods /// /// Gets the for the Creator of this media item. /// public static IProfile GetCreatorProfile(this IMedia media, IUserService userService) { return userService.GetProfileById(media.CreatorId); } [Obsolete("Use the overload that declares the IUserService to use")] [EditorBrowsable(EditorBrowsableState.Never)] public static IProfile GetCreatorProfile(this IContentBase content) { return 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); } [Obsolete("Use the overload that declares the IUserService to use")] [EditorBrowsable(EditorBrowsableState.Never)] public static IProfile GetWriterProfile(this IContent content) { return 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); } /// /// Gets the for the Writer of this content. /// public static IProfile GetWriterProfile(this IMedia content, IUserService userService) { return userService.GetProfileById(content.WriterId); } #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); } [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IContent content) { return 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); } [Obsolete("Use the overload that declares the IPackagingService to use")] public static XElement ToXml(this IMedia media) { return 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 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 public static XElement ToXml(this IMember member, IPackagingService packagingService) { return ((PackagingService)(packagingService)).Export(member); } #endregion #region Dirty 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()); } #endregion } }