using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
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
{
// 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);
}
[Obsolete("Use the overload with the service reference instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static IEnumerable Ancestors(this IMedia media)
{
return 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, IMediaService mediaService)
{
return mediaService.GetChildren(media.Id);
}
[Obsolete("Use the overload with the service reference instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static IEnumerable Children(this IMedia media)
{
return 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, IMediaService mediaService)
{
return mediaService.GetDescendants(media);
}
[Obsolete("Use the overload with the service reference instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static IEnumerable Descendants(this IMedia media)
{
return Current.Services.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);
}
[Obsolete("Use the overload with the service reference instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static IMedia Parent(this IMedia media)
{
return Current.Services.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
///
/// 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(); // fixme - er... why?
MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream);
}
///
/// 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(); // fixme - er... why?
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 MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath);
}
#endregion
#region User/Profile methods
[Obsolete("Use the overload that declares the IUserService to use")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static IProfile GetCreatorProfile(this IMedia media)
{
return 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);
}
[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 Tags
///
/// Sets tags.
///
/// The content item.
/// The property alias.
/// The tags.
/// True to replace the tags with the specified tags or false to merge them with the currently assigned ones.
/// The tags group.
/// The tags storage type.
public static void SetTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool replaceTags = true, string tagGroup = "default", TagCacheStorageType storage = TagCacheStorageType.Csv)
{
var property = content.Properties[propertyTypeAlias];
if (property == null)
throw new IndexOutOfRangeException("No property exists with name " + propertyTypeAlias);
property.SetTags(propertyTypeAlias, tags, replaceTags, tagGroup, storage);
}
// fixme - totally not ok with variants
internal static void SetTags(this Property property, string propertyTypeAlias, IEnumerable tags, bool replaceTags = true, string tagGroup = "default", TagCacheStorageType storage = TagCacheStorageType.Csv)
{
if (property == null) throw new ArgumentNullException(nameof(property));
var trimmedTags = tags.Select(x => x.Trim()).ToArray();
var changes = property.TagChanges;
changes.Add(new PropertyTagChange
{
Type = replaceTags ? PropertyTagChange.ChangeType.Replace : PropertyTagChange.ChangeType.Merge,
Tags = trimmedTags.Select(x => new Tuple(x, tagGroup))
});
// ensure the property value is set to the same thing
if (replaceTags)
{
switch (storage)
{
case TagCacheStorageType.Csv:
property.SetValue(string.Join(",", trimmedTags)); // csv string
break;
case TagCacheStorageType.Json:
property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array
break;
}
}
else // merge
{
IEnumerable currentTags;
switch (storage)
{
case TagCacheStorageType.Csv:
currentTags = property.GetValue().ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim());
property.SetValue(string.Join(",", currentTags.Union(trimmedTags))); // csv string
break;
case TagCacheStorageType.Json:
currentTags = JsonConvert.DeserializeObject(property.GetValue().ToString()).Select(x => x.ToString());
property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array
break;
}
}
}
///
/// Remove tags.
///
/// The content item.
/// The property alias.
/// The tags.
/// The tags group.
// fixme - totally not ok with variants
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();
var changes = property.TagChanges;
changes.Add(new PropertyTagChange
{
Type = PropertyTagChange.ChangeType.Remove,
Tags = trimmedTags.Select(x => new Tuple(x, tagGroup))
});
// set the property value
var value = property.GetValue()?.ToString();
if (string.IsNullOrWhiteSpace(value)) return;
var storage = value.StartsWith("[") ? TagCacheStorageType.Json : TagCacheStorageType.Csv;
IEnumerable currentTags;
switch (storage)
{
case TagCacheStorageType.Csv:
currentTags = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim());
property.SetValue(string.Join(",", currentTags.Except(trimmedTags)));
break;
case TagCacheStorageType.Json:
currentTags = JsonConvert.DeserializeObject(property.GetValue().ToString()).Select(x => x.ToString());
property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array
break;
}
}
#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);
}
[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);
}
[Obsolete("Use the overload that declares the IPackagingService to use")]
public static XElement ToXml(this IMember member)
{
return ((PackagingService)(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
#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
}
}