Files
Umbraco-CMS/src/Umbraco.Core/Extensions/ContentExtensions.cs

410 lines
17 KiB
C#
Raw Normal View History

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
2021-10-06 14:30:26 +02:00
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
2021-10-06 14:30:26 +02:00
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Extensions
{
public static class ContentExtensions
{
/// <summary>
/// Returns the path to a media item stored in a property if the property editor is <see cref="IMediaUrlGenerator"/>
/// </summary>
/// <param name="content"></param>
/// <param name="propertyTypeAlias"></param>
/// <param name="mediaUrlGenerators"></param>
/// <param name="mediaFilePath"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns>True if the file path can be resolved and the property is <see cref="IMediaUrlGenerator"/></returns>
public static bool TryGetMediaPath(
this IContentBase content,
string propertyTypeAlias,
MediaUrlGeneratorCollection mediaUrlGenerators,
out string mediaFilePath,
string culture = null,
string segment = null)
{
if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty property))
{
mediaFilePath = null;
return false;
}
if (!mediaUrlGenerators.TryGetMediaPath(
property.PropertyType.PropertyEditorAlias,
property.GetValue(culture, segment),
out mediaFilePath))
{
return false;
}
return true;
}
public static bool IsAnyUserPropertyDirty(this IContentBase entity)
{
return entity.Properties.Any(x => x.IsDirty());
}
2018-06-29 19:52:40 +02:00
public static bool WasAnyUserPropertyDirty(this IContentBase entity)
{
return entity.Properties.Any(x => x.WasDirty());
}
public static bool IsMoving(this IContentBase entity)
{
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below
// operations which will make this whole operation go much faster. When moving we don't need to create
// new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsPropertyDirty(nameof(entity.Path))
&& entity.IsPropertyDirty(nameof(entity.Level))
&& entity.IsPropertyDirty(nameof(entity.UpdateDate));
return isMoving;
}
2018-06-29 19:52:40 +02:00
/// <summary>
2019-01-22 18:03:39 -05:00
/// Removes characters that are not valid XML characters from all entity properties
2018-06-29 19:52:40 +02:00
/// of type string. See: http://stackoverflow.com/a/961504/5018
/// </summary>
/// <returns></returns>
/// <remarks>
/// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it.
/// </remarks>
/// <param name="entity"></param>
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();
}
}
}
/// <summary>
/// Checks if the IContentBase has children
/// </summary>
/// <param name="content"></param>
/// <param name="services"></param>
/// <returns></returns>
/// <remarks>
/// This is a bit of a hack because we need to type check!
/// </remarks>
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;
}
/// <summary>
/// Returns all properties based on the editorAlias
/// </summary>
/// <param name="content"></param>
/// <param name="editorAlias"></param>
/// <returns></returns>
public static IEnumerable<IProperty> GetPropertiesByEditor(this IContentBase content, string editorAlias)
=> content.Properties.Where(x => x.PropertyType.PropertyEditorAlias == editorAlias);
#region IContent
/// <summary>
/// Gets the current status of the Content
/// </summary>
public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string culture = null)
{
if (content.Trashed)
return ContentStatus.Trashed;
if (!content.ContentType.VariesByCulture())
culture = string.Empty;
else if (culture.IsNullOrWhiteSpace())
throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty");
var expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire);
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
return ContentStatus.Expired;
var release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release);
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
return ContentStatus.AwaitingRelease;
if (content.Published)
return ContentStatus.Published;
return ContentStatus.Unpublished;
}
2021-10-06 14:30:26 +02:00
/// <summary>
/// Gets a collection containing the ids of all ancestors.
/// </summary>
/// <param name="content"><see cref="IContent"/> to retrieve ancestors for</param>
/// <returns>An Enumerable list of integer ids</returns>
public static IEnumerable<int> GetAncestorIds(this IContent content) =>
content.Path.Split(Constants.CharArrays.Comma)
.Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(s =>
int.Parse(s, CultureInfo.InvariantCulture));
#endregion
/// <summary>
/// Gets the <see cref="IProfile"/> for the Creator of this content item.
/// </summary>
public static IProfile GetCreatorProfile(this IContentBase content, IUserService userService)
{
return userService.GetProfileById(content.CreatorId);
}
/// <summary>
/// Gets the <see cref="IProfile"/> for the Writer of this content.
/// </summary>
public static IProfile GetWriterProfile(this IContent content, IUserService userService)
{
return userService.GetProfileById(content.WriterId);
}
/// <summary>
/// Gets the <see cref="IProfile"/> for the Writer of this content.
/// </summary>
public static IProfile GetWriterProfile(this IMedia content, IUserService userService)
{
return userService.GetProfileById(content.WriterId);
}
#region User/Profile methods
/// <summary>
/// Gets the <see cref="IProfile"/> for the Creator of this media item.
/// </summary>
public static IProfile GetCreatorProfile(this IMedia media, IUserService userService)
{
return userService.GetProfileById(media.CreatorId);
}
#endregion
/// <summary>
/// Returns properties that do not belong to a group
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public static IEnumerable<IProperty> GetNonGroupedProperties(this IContentBase content)
{
return content.Properties
.Where(x => x.PropertyType.PropertyGroupId == null)
.OrderBy(x => x.PropertyType.SortOrder);
}
/// <summary>
/// Returns the Property object for the given property group
/// </summary>
/// <param name="content"></param>
/// <param name="propertyGroup"></param>
/// <returns></returns>
public static IEnumerable<IProperty> 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));
}
#region SetValue for setting file contents
/// <summary>
/// Sets the posted file value of a property.
/// </summary>
public static void SetValue(
this IContentBase content,
MediaFileManager mediaFileManager,
MediaUrlGeneratorCollection mediaUrlGenerators,
IShortStringHelper shortStringHelper,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
string propertyTypeAlias,
string filename,
Stream filestream,
string culture = null,
string segment = null)
{
if (filename == null || filestream == null)
return;
filename = shortStringHelper.CleanStringForSafeFileName(filename);
if (string.IsNullOrWhiteSpace(filename))
return;
filename = filename.ToLower();
SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment);
}
private static void SetUploadFile(
this IContentBase content,
MediaFileManager mediaFileManager,
MediaUrlGeneratorCollection mediaUrlGenerators,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
string propertyTypeAlias,
string filename,
Stream filestream,
string culture = null,
string segment = null)
{
var property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias);
// Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an
// existing IMedia with extension SetValue causes exception 'Illegal characters in path'
string oldpath = null;
if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out string mediaFilePath, culture, segment))
{
oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath);
}
Netcore: File systems rework (#10181) * Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem * Remove GetFileSystem from Filesystems It was only used by tests. * Make MediaFileSystem inherit from PhysicalFileSystem directly * Remove FileSystemWrapper * Remove inner filesystem from MediaFileSystem * Add MediaFileManager and bare minimum to make it testable * Remove MediaFileSystem * Fix unit tests using MediaFileManager * Remove IFileSystem and rely only on FileSystem * Hide dangerous methods in FileSystems and do some cleaning * Apply stylecop warnings to MediaFileManager * Add FilesystemsCreator to Tests.Common This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite. * Allow the stylesheet filesystem to be replaced. * Fix tests * Don't save stylesheetWrapper in a temporary var * refactor(FileSystems): change how stylesheet filesystem is registered * fix(FileSystems): unable to overwrite media filesystem SetMediaFileSystem added the MediaManager as a Singleton instead of replacing the existing instance. * fix(FileSystems): calling AddFileSystems replaces MediaManager When calling AddFileSystems after SetMediaFileSystem the MediaManager gets replaced by the default PhysicalFileSystem, so instead of calling SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem instead. This method will not replace any existing instance of the MediaManager if there's already a MediaManager registered. * Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems Also don't call AddFileSystems again in ConfigureFilesystems * Don't wrap CSS filesystem twice * Add CreateShadowWrapperInternal to avoid casting * Throw UnauthorizedAccessException isntead of InvalidOperationException * Remove ResetShadowId Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00
var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath);
// NOTE: Here we are just setting the value to a string which means that any file based editor
// will need to handle the raw string value and save it to it's correct (i.e. JSON)
// format. I'm unsure how this works today with image cropper but it does (maybe events?)
Netcore: File systems rework (#10181) * Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem * Remove GetFileSystem from Filesystems It was only used by tests. * Make MediaFileSystem inherit from PhysicalFileSystem directly * Remove FileSystemWrapper * Remove inner filesystem from MediaFileSystem * Add MediaFileManager and bare minimum to make it testable * Remove MediaFileSystem * Fix unit tests using MediaFileManager * Remove IFileSystem and rely only on FileSystem * Hide dangerous methods in FileSystems and do some cleaning * Apply stylecop warnings to MediaFileManager * Add FilesystemsCreator to Tests.Common This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite. * Allow the stylesheet filesystem to be replaced. * Fix tests * Don't save stylesheetWrapper in a temporary var * refactor(FileSystems): change how stylesheet filesystem is registered * fix(FileSystems): unable to overwrite media filesystem SetMediaFileSystem added the MediaManager as a Singleton instead of replacing the existing instance. * fix(FileSystems): calling AddFileSystems replaces MediaManager When calling AddFileSystems after SetMediaFileSystem the MediaManager gets replaced by the default PhysicalFileSystem, so instead of calling SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem instead. This method will not replace any existing instance of the MediaManager if there's already a MediaManager registered. * Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems Also don't call AddFileSystems again in ConfigureFilesystems * Don't wrap CSS filesystem twice * Add CreateShadowWrapperInternal to avoid casting * Throw UnauthorizedAccessException isntead of InvalidOperationException * Remove ResetShadowId Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00
property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment);
}
// gets or creates a property for a content item.
private static IProperty GetProperty(IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias)
{
var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
if (property != null)
return property;
var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content);
var propertyType = contentType.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;
}
/// <summary>
/// Stores a file.
/// </summary>
/// <param name="content"><see cref="IContentBase"/>A content item.</param>
/// <param name="propertyTypeAlias">The property alias.</param>
/// <param name="filename">The name of the file.</param>
/// <param name="filestream">A stream containing the file data.</param>
/// <param name="filepath">The original file path, if any.</param>
/// <returns>The path to the file, relative to the media filesystem.</returns>
/// <remarks>
/// <para>Does NOT set the property value, so one should probably store the file and then do
/// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath).</para>
/// <para>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.</para>
/// </remarks>
Netcore: File systems rework (#10181) * Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem * Remove GetFileSystem from Filesystems It was only used by tests. * Make MediaFileSystem inherit from PhysicalFileSystem directly * Remove FileSystemWrapper * Remove inner filesystem from MediaFileSystem * Add MediaFileManager and bare minimum to make it testable * Remove MediaFileSystem * Fix unit tests using MediaFileManager * Remove IFileSystem and rely only on FileSystem * Hide dangerous methods in FileSystems and do some cleaning * Apply stylecop warnings to MediaFileManager * Add FilesystemsCreator to Tests.Common This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite. * Allow the stylesheet filesystem to be replaced. * Fix tests * Don't save stylesheetWrapper in a temporary var * refactor(FileSystems): change how stylesheet filesystem is registered * fix(FileSystems): unable to overwrite media filesystem SetMediaFileSystem added the MediaManager as a Singleton instead of replacing the existing instance. * fix(FileSystems): calling AddFileSystems replaces MediaManager When calling AddFileSystems after SetMediaFileSystem the MediaManager gets replaced by the default PhysicalFileSystem, so instead of calling SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem instead. This method will not replace any existing instance of the MediaManager if there's already a MediaManager registered. * Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems Also don't call AddFileSystems again in ConfigureFilesystems * Don't wrap CSS filesystem twice * Add CreateShadowWrapperInternal to avoid casting * Throw UnauthorizedAccessException isntead of InvalidOperationException * Remove ResetShadowId Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00
public static string StoreFile(this IContentBase content, MediaFileManager mediaFileManager, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string filepath)
{
var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content);
var propertyType = contentType
.CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
if (propertyType == null)
throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + ".");
Netcore: File systems rework (#10181) * Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem * Remove GetFileSystem from Filesystems It was only used by tests. * Make MediaFileSystem inherit from PhysicalFileSystem directly * Remove FileSystemWrapper * Remove inner filesystem from MediaFileSystem * Add MediaFileManager and bare minimum to make it testable * Remove MediaFileSystem * Fix unit tests using MediaFileManager * Remove IFileSystem and rely only on FileSystem * Hide dangerous methods in FileSystems and do some cleaning * Apply stylecop warnings to MediaFileManager * Add FilesystemsCreator to Tests.Common This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite. * Allow the stylesheet filesystem to be replaced. * Fix tests * Don't save stylesheetWrapper in a temporary var * refactor(FileSystems): change how stylesheet filesystem is registered * fix(FileSystems): unable to overwrite media filesystem SetMediaFileSystem added the MediaManager as a Singleton instead of replacing the existing instance. * fix(FileSystems): calling AddFileSystems replaces MediaManager When calling AddFileSystems after SetMediaFileSystem the MediaManager gets replaced by the default PhysicalFileSystem, so instead of calling SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem instead. This method will not replace any existing instance of the MediaManager if there's already a MediaManager registered. * Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems Also don't call AddFileSystems again in ConfigureFilesystems * Don't wrap CSS filesystem twice * Add CreateShadowWrapperInternal to avoid casting * Throw UnauthorizedAccessException isntead of InvalidOperationException * Remove ResetShadowId Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00
return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath);
}
#endregion
#region Dirty
public static IEnumerable<string> GetDirtyUserProperties(this IContentBase entity)
{
return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias);
}
#endregion
/// <summary>
/// Creates the full xml representation for the <see cref="IContent"/> object and all of it's descendants
/// </summary>
/// <param name="content"><see cref="IContent"/> to generate xml for</param>
/// <param name="serializer"></param>
/// <returns>Xml representation of the passed in <see cref="IContent"/></returns>
public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer)
{
return serializer.Serialize(content, false, true);
}
/// <summary>
/// Creates the xml representation for the <see cref="IContent"/> object
/// </summary>
/// <param name="content"><see cref="IContent"/> to generate xml for</param>
/// <param name="serializer"></param>
/// <returns>Xml representation of the passed in <see cref="IContent"/></returns>
public static XElement ToXml(this IContent content, IEntityXmlSerializer serializer)
{
return serializer.Serialize(content, false, false);
}
/// <summary>
/// Creates the xml representation for the <see cref="IMedia"/> object
/// </summary>
/// <param name="media"><see cref="IContent"/> to generate xml for</param>
/// <param name="serializer"></param>
/// <returns>Xml representation of the passed in <see cref="IContent"/></returns>
public static XElement ToXml(this IMedia media, IEntityXmlSerializer serializer)
{
return serializer.Serialize(media);
}
/// <summary>
/// Creates the xml representation for the <see cref="IMember"/> object
/// </summary>
/// <param name="member"><see cref="IMember"/> to generate xml for</param>
/// <param name="serializer"></param>
/// <returns>Xml representation of the passed in <see cref="IContent"/></returns>
public static XElement ToXml(this IMember member, IEntityXmlSerializer serializer)
{
return serializer.Serialize(member);
}
}
}