Fixes issues with creation of documents from blueprints that have populated file upload properties (#19655)

* Fixes issue where content created from blueprint would not persist file upload property values.

* Ensure a copy of a file upload is created when scaffolding content from a blueprint, like we do when copying content.

* Clarified comment.

* Removed unneeded usings.

* Fixed spelling.

* Handle create of blueprint from content to create a new uploaded file.
Handle delete of blueprint to delete uploaded files.
This commit is contained in:
Andy Butland
2025-07-07 14:15:17 +02:00
committed by GitHub
parent 4c87cc5fd9
commit b5195ed8eb
14 changed files with 314 additions and 340 deletions

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IContentService when the SavedBlueprint method is called in the API.
/// </summary>
@@ -14,8 +15,21 @@ public sealed class ContentSavedBlueprintNotification : ObjectNotification<ICont
: base(target, messages)
{
}
public ContentSavedBlueprintNotification(IContent target, IContent? createdFromContent, EventMessages messages)
: base(target, messages)
{
CreatedFromContent = createdFromContent;
}
/// <summary>
/// Getting the saved blueprint <see cref="IContent"/> object.
/// </summary>
public IContent SavedBlueprint => Target;
/// <summary>
/// Getting the saved blueprint <see cref="IContent"/> object.
/// </summary>
public IContent? CreatedFromContent { get; }
}

View File

@@ -122,7 +122,7 @@ internal sealed class ContentBlueprintEditingService
}
// Save blueprint
await SaveAsync(blueprint, userKey);
await SaveAsync(blueprint, userKey, content);
return Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, new ContentCreateResult { Content = blueprint });
}
@@ -240,10 +240,10 @@ internal sealed class ContentBlueprintEditingService
protected override OperationResult? Delete(IContent content, int userId) => throw new NotImplementedException();
private async Task SaveAsync(IContent blueprint, Guid userKey)
private async Task SaveAsync(IContent blueprint, Guid userKey, IContent? createdFromContent = null)
{
var currentUserId = await GetUserIdAsync(userKey);
ContentService.SaveBlueprint(blueprint, currentUserId);
ContentService.SaveBlueprint(blueprint, createdFromContent, currentUserId);
}
private bool ValidateUniqueName(string name, IContent content)

View File

@@ -3611,6 +3611,9 @@ public class ContentService : RepositoryService, IContentService
}
public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
=> SaveBlueprint(content, null, userId);
public void SaveBlueprint(IContent content, IContent? createdFromContent, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
@@ -3631,7 +3634,7 @@ public class ContentService : RepositoryService, IContentService
Audit(AuditType.Save, userId, content.Id, $"Saved content template: {content.Name}");
scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, createdFromContent, evtMsgs));
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, evtMsgs));
scope.Complete();

View File

@@ -47,8 +47,17 @@ public interface IContentService : IContentServiceBase<IContent>
/// <summary>
/// Saves a blueprint.
/// </summary>
[Obsolete("Please use the method taking all parameters. Scheduled for removal in Umbraco 18.")]
void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Saves a blueprint.
/// </summary>
void SaveBlueprint(IContent content, IContent? createdFromContent, int userId = Constants.Security.SuperUserId)
#pragma warning disable CS0618 // Type or member is obsolete
=> SaveBlueprint(content, userId);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Deletes a blueprint.
/// </summary>

View File

@@ -356,11 +356,14 @@ public static partial class UmbracoBuilderExtensions
.AddNotificationHandler<ContentSavingNotification, RichTextPropertyNotificationHandler>()
.AddNotificationHandler<ContentCopyingNotification, RichTextPropertyNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, RichTextPropertyNotificationHandler>()
.AddNotificationHandler<ContentCopiedNotification, FileUploadContentCopiedNotificationHandler>()
.AddNotificationHandler<ContentCopiedNotification, FileUploadContentCopiedOrScaffoldedNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, FileUploadContentCopiedOrScaffoldedNotificationHandler>()
.AddNotificationHandler<ContentSavedBlueprintNotification, FileUploadContentCopiedOrScaffoldedNotificationHandler>()
.AddNotificationHandler<ContentDeletedNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MediaDeletedNotification, FileUploadMediaDeletedNotificationHandler>()
.AddNotificationHandler<ContentDeletedBlueprintNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MediaDeletedNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MemberDeletedNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MediaSavingNotification, FileUploadMediaSavingNotificationHandler>()
.AddNotificationHandler<MemberDeletedNotification, FileUploadMemberDeletedNotificationHandler>()
.AddNotificationHandler<ContentCopiedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<ContentDeletedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<MediaDeletedNotification, ImageCropperPropertyEditor>()

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
@@ -63,7 +63,7 @@ public class MoveDocumentBlueprintsToFolders : MigrationBase
}
blueprint.ParentId = container.Id;
_contentService.SaveBlueprint(blueprint);
_contentService.SaveBlueprint(blueprint, null);
}
}
}

View File

@@ -92,13 +92,10 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
{
FileUploadValue? editorModelValue = _valueParser.Parse(editorValue.Value);
// no change?
// No change or created from blueprint.
if (editorModelValue?.TemporaryFileId.HasValue is not true && string.IsNullOrEmpty(editorModelValue?.Src) is false)
{
// since current value can be json string, we have to parse value
FileUploadValue? currentModelValue = _valueParser.Parse(currentValue);
return currentModelValue?.Src;
return editorModelValue.Src;
}
// the current editor value (if any) is the path to the file

View File

@@ -14,24 +14,26 @@ using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
/// <summary>
/// Implements a notification handler that processes file uploads when content is copied, making sure the copied contetnt relates to a new instance
/// of the file.
/// Implements a notification handler that processes file uploads when content is copied or scaffolded from a blueprint, making
/// sure the new content references a new instance of the file.
/// </summary>
internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNotificationHandlerBase, INotificationHandler<ContentCopiedNotification>
internal sealed class FileUploadContentCopiedOrScaffoldedNotificationHandler : FileUploadNotificationHandlerBase,
INotificationHandler<ContentCopiedNotification>,
INotificationHandler<ContentScaffoldedNotification>,
INotificationHandler<ContentSavedBlueprintNotification>
{
private readonly IContentService _contentService;
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockListEditorValues;
private readonly BlockEditorValues<BlockGridValue, BlockGridLayoutItem> _blockGridEditorValues;
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadContentCopiedNotificationHandler"/> class.
/// Initializes a new instance of the <see cref="FileUploadContentCopiedOrScaffoldedNotificationHandler"/> class.
/// </summary>
public FileUploadContentCopiedNotificationHandler(
public FileUploadContentCopiedOrScaffoldedNotificationHandler(
IJsonSerializer jsonSerializer,
MediaFileManager mediaFileManager,
IBlockEditorElementTypeCache elementTypeCache,
ILogger<FileUploadContentCopiedNotificationHandler> logger,
ILogger<FileUploadContentCopiedOrScaffoldedNotificationHandler> logger,
IContentService contentService)
: base(jsonSerializer, mediaFileManager, elementTypeCache)
{
@@ -41,51 +43,66 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
}
/// <inheritdoc/>
public void Handle(ContentCopiedNotification notification)
{
ArgumentNullException.ThrowIfNull(notification);
public void Handle(ContentCopiedNotification notification) => Handle(notification.Original, notification.Copy, (IContent c) => _contentService.Save(c));
/// <inheritdoc/>
public void Handle(ContentScaffoldedNotification notification) => Handle(notification.Original, notification.Scaffold);
/// <inheritdoc/>
public void Handle(ContentSavedBlueprintNotification notification)
{
if (notification.CreatedFromContent is null)
{
// If there is no original content, we don't need to copy files.
return;
}
Handle(notification.CreatedFromContent, notification.SavedBlueprint, (IContent c) => _contentService.SaveBlueprint(c, null));
}
private void Handle(IContent source, IContent destination, Action<IContent>? postUpdateAction = null)
{
var isUpdated = false;
foreach (IProperty property in notification.Original.Properties)
foreach (IProperty property in source.Properties)
{
if (IsUploadFieldPropertyType(property.PropertyType))
{
isUpdated |= UpdateUploadFieldProperty(notification, property);
isUpdated |= UpdateUploadFieldProperty(destination, property);
continue;
}
if (IsBlockListPropertyType(property.PropertyType))
{
isUpdated |= UpdateBlockProperty(notification, property, _blockListEditorValues);
isUpdated |= UpdateBlockProperty(destination, property, _blockListEditorValues);
continue;
}
if (IsBlockGridPropertyType(property.PropertyType))
{
isUpdated |= UpdateBlockProperty(notification, property, _blockGridEditorValues);
isUpdated |= UpdateBlockProperty(destination, property, _blockGridEditorValues);
continue;
}
if (IsRichTextPropertyType(property.PropertyType))
{
isUpdated |= UpdateRichTextProperty(notification, property);
isUpdated |= UpdateRichTextProperty(destination, property);
continue;
}
}
// if updated, re-save the copy with the updated value
if (isUpdated)
// If updated, re-save the destination with the updated value.
if (isUpdated && postUpdateAction is not null)
{
_contentService.Save(notification.Copy);
postUpdateAction(destination);
}
}
private bool UpdateUploadFieldProperty(ContentCopiedNotification notification, IProperty property)
private bool UpdateUploadFieldProperty(IContent content, IProperty property)
{
var isUpdated = false;
@@ -98,9 +115,9 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
continue;
}
var copyUrl = CopyFile(sourceUrl, notification.Copy, property.PropertyType);
var copyUrl = CopyFile(sourceUrl, content, property.PropertyType);
notification.Copy.SetValue(property.Alias, copyUrl, propertyValue.Culture, propertyValue.Segment);
content.SetValue(property.Alias, copyUrl, propertyValue.Culture, propertyValue.Segment);
isUpdated = true;
}
@@ -108,7 +125,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
return isUpdated;
}
private bool UpdateBlockProperty<TValue, TLayout>(ContentCopiedNotification notification, IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
private bool UpdateBlockProperty<TValue, TLayout>(IContent content, IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
@@ -120,11 +137,11 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
BlockEditorData<TValue, TLayout>? blockEditorData = GetBlockEditorData(rawBlockPropertyValue, blockEditorValues);
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(notification, blockEditorData);
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(content, blockEditorData);
if (hasUpdates)
{
notification.Copy.SetValue(property.Alias, updatedValue, blockPropertyValue.Culture, blockPropertyValue.Segment);
content.SetValue(property.Alias, updatedValue, blockPropertyValue.Culture, blockPropertyValue.Segment);
}
isUpdated |= hasUpdates;
@@ -133,7 +150,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
return isUpdated;
}
private (bool, string?) UpdateBlockEditorData<TValue, TLayout>(ContentCopiedNotification notification, BlockEditorData<TValue, TLayout>? blockEditorData)
private (bool, string?) UpdateBlockEditorData<TValue, TLayout>(IContent content, BlockEditorData<TValue, TLayout>? blockEditorData)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
@@ -148,14 +165,14 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
.Concat(blockEditorData.BlockValue.SettingsData)
.SelectMany(x => x.Values);
isUpdated = UpdateBlockPropertyValues(notification, isUpdated, blockPropertyValues);
isUpdated = UpdateBlockPropertyValues(content, isUpdated, blockPropertyValues);
var updatedValue = JsonSerializer.Serialize(blockEditorData.BlockValue);
return (isUpdated, updatedValue);
}
private bool UpdateRichTextProperty(ContentCopiedNotification notification, IProperty property)
private bool UpdateRichTextProperty(IContent content, IProperty property)
{
var isUpdated = false;
@@ -165,7 +182,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
RichTextBlockValue? richTextBlockValue = GetRichTextBlockValue(rawBlockPropertyValue);
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(notification, richTextBlockValue);
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(content, richTextBlockValue);
if (hasUpdates && string.IsNullOrEmpty(updatedValue) is false)
{
@@ -173,7 +190,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
if (richTextEditorValue is not null)
{
richTextEditorValue.Blocks = JsonSerializer.Deserialize<RichTextBlockValue>(updatedValue);
notification.Copy.SetValue(property.Alias, JsonSerializer.Serialize(richTextEditorValue), blockPropertyValue.Culture, blockPropertyValue.Segment);
content.SetValue(property.Alias, JsonSerializer.Serialize(richTextEditorValue), blockPropertyValue.Culture, blockPropertyValue.Segment);
}
}
@@ -183,7 +200,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
return isUpdated;
}
private (bool, string?) UpdateBlockEditorData(ContentCopiedNotification notification, RichTextBlockValue? richTextBlockValue)
private (bool, string?) UpdateBlockEditorData(IContent content, RichTextBlockValue? richTextBlockValue)
{
var isUpdated = false;
@@ -196,14 +213,14 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
.Concat(richTextBlockValue.SettingsData)
.SelectMany(x => x.Values);
isUpdated = UpdateBlockPropertyValues(notification, isUpdated, blockPropertyValues);
isUpdated = UpdateBlockPropertyValues(content, isUpdated, blockPropertyValues);
var updatedValue = JsonSerializer.Serialize(richTextBlockValue);
return (isUpdated, updatedValue);
}
private bool UpdateBlockPropertyValues(ContentCopiedNotification notification, bool isUpdated, IEnumerable<BlockPropertyValue> blockPropertyValues)
private bool UpdateBlockPropertyValues(IContent content, bool isUpdated, IEnumerable<BlockPropertyValue> blockPropertyValues)
{
foreach (BlockPropertyValue blockPropertyValue in blockPropertyValues)
{
@@ -221,14 +238,14 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
if (IsUploadFieldPropertyType(propertyType))
{
isUpdated |= UpdateUploadFieldBlockPropertyValue(blockPropertyValue, notification, propertyType);
isUpdated |= UpdateUploadFieldBlockPropertyValue(blockPropertyValue, content, propertyType);
continue;
}
if (IsBlockListPropertyType(propertyType))
{
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, notification, _blockListEditorValues);
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, content, _blockListEditorValues);
isUpdated |= hasUpdates;
@@ -239,7 +256,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
if (IsBlockGridPropertyType(propertyType))
{
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, notification, _blockGridEditorValues);
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, content, _blockGridEditorValues);
isUpdated |= hasUpdates;
@@ -250,7 +267,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
if (IsRichTextPropertyType(propertyType))
{
(bool hasUpdates, string? newValue) = UpdateRichTextPropertyValue(blockPropertyValue, notification);
(bool hasUpdates, string? newValue) = UpdateRichTextPropertyValue(blockPropertyValue, content);
if (hasUpdates && string.IsNullOrEmpty(newValue) is false)
{
@@ -271,7 +288,7 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
return isUpdated;
}
private bool UpdateUploadFieldBlockPropertyValue(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification, IPropertyType propertyType)
private bool UpdateUploadFieldBlockPropertyValue(BlockPropertyValue blockItemDataValue, IContent content, IPropertyType propertyType)
{
FileUploadValue? fileUploadValue = FileUploadValueParser.Parse(blockItemDataValue.Value);
@@ -281,26 +298,26 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot
return false;
}
var copyFileUrl = CopyFile(fileUploadValue.Src, notification.Copy, propertyType);
var copyFileUrl = CopyFile(fileUploadValue.Src, content, propertyType);
blockItemDataValue.Value = copyFileUrl;
return true;
}
private (bool, string?) UpdateBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification, BlockEditorValues<TValue, TLayout> blockEditorValues)
private (bool, string?) UpdateBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, IContent content, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
BlockEditorData<TValue, TLayout>? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues);
return UpdateBlockEditorData(notification, blockItemEditorDataValue);
return UpdateBlockEditorData(content, blockItemEditorDataValue);
}
private (bool, string?) UpdateRichTextPropertyValue(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification)
private (bool, string?) UpdateRichTextPropertyValue(BlockPropertyValue blockItemDataValue, IContent content)
{
RichTextBlockValue? richTextBlockValue = GetRichTextBlockValue(blockItemDataValue.Value);
return UpdateBlockEditorData(notification, richTextBlockValue);
return UpdateBlockEditorData(content, richTextBlockValue);
}
private string CopyFile(string sourceUrl, IContent destinationContent, IPropertyType propertyType)

View File

@@ -1,17 +1,30 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Extensions;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
/// <summary>
/// Implements a notification handler that processes file uploads when content is deleted, removing associated files.
/// Provides base class for notification handler that processes file uploads when a content entity is deleted, removing associated files.
/// </summary>
internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<ContentDeletedNotification>
internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadNotificationHandlerBase,
INotificationHandler<ContentDeletedNotification>,
INotificationHandler<ContentDeletedBlueprintNotification>,
INotificationHandler<MediaDeletedNotification>,
INotificationHandler<MemberDeletedNotification>
{
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockListEditorValues;
private readonly BlockEditorValues<BlockGridValue, BlockGridLayoutItem> _blockGridEditorValues;
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadContentDeletedNotificationHandler"/> class.
/// </summary>
@@ -20,10 +33,204 @@ internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadEn
MediaFileManager mediaFileManager,
IBlockEditorElementTypeCache elementTypeCache,
ILogger<FileUploadContentDeletedNotificationHandler> logger)
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
: base(jsonSerializer, mediaFileManager, elementTypeCache)
{
_blockListEditorValues = new(new BlockListEditorDataConverter(jsonSerializer), elementTypeCache, logger);
_blockGridEditorValues = new(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger);
}
/// <inheritdoc/>
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
/// <inheritdoc/>
public void Handle(ContentDeletedBlueprintNotification notification) => DeleteContainedFiles(notification.DeletedBlueprints);
/// <inheritdoc/>
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
/// <inheritdoc/>
public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
/// <summary>
/// Deletes all file upload property files contained within a collection of content entities.
/// </summary>
/// <param name="deletedEntities"></param>
private void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
{
IReadOnlyList<string> filePathsToDelete = ContainedFilePaths(deletedEntities);
MediaFileManager.DeleteMediaFiles(filePathsToDelete);
}
/// <summary>
/// Gets the paths to all file upload property files contained within a collection of content entities.
/// </summary>
private IReadOnlyList<string> ContainedFilePaths(IEnumerable<IContentBase> entities)
{
var paths = new List<string>();
foreach (IProperty? property in entities.SelectMany(x => x.Properties))
{
if (IsUploadFieldPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromUploadFieldProperty(property));
continue;
}
if (IsBlockListPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromBlockProperty(property, _blockListEditorValues));
continue;
}
if (IsBlockGridPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromBlockProperty(property, _blockGridEditorValues));
continue;
}
if (IsRichTextPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromRichTextProperty(property));
continue;
}
}
return paths.Distinct().ToList().AsReadOnly();
}
private IEnumerable<string> GetPathsFromUploadFieldProperty(IProperty property)
{
foreach (IPropertyValue propertyValue in property.Values)
{
if (propertyValue.PublishedValue != null && propertyValue.PublishedValue is string publishedUrl && !string.IsNullOrWhiteSpace(publishedUrl))
{
yield return MediaFileManager.FileSystem.GetRelativePath(publishedUrl);
}
if (propertyValue.EditedValue != null && propertyValue.EditedValue is string editedUrl && !string.IsNullOrWhiteSpace(editedUrl))
{
yield return MediaFileManager.FileSystem.GetRelativePath(editedUrl);
}
}
}
private IReadOnlyCollection<string> GetPathsFromBlockProperty<TValue, TLayout>(IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
var paths = new List<string>();
foreach (IPropertyValue blockPropertyValue in property.Values)
{
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.PublishedValue, blockEditorValues)?.BlockValue));
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.EditedValue, blockEditorValues)?.BlockValue));
}
return paths;
}
private IReadOnlyCollection<string> GetPathsFromBlockValue(BlockValue? blockValue)
{
var paths = new List<string>();
if (blockValue is null)
{
return paths;
}
IEnumerable<BlockPropertyValue> blockPropertyValues = blockValue.ContentData
.Concat(blockValue.SettingsData)
.SelectMany(x => x.Values);
foreach (BlockPropertyValue blockPropertyValue in blockPropertyValues)
{
if (blockPropertyValue.Value == null)
{
continue;
}
IPropertyType? propertyType = blockPropertyValue.PropertyType;
if (propertyType == null)
{
continue;
}
if (IsUploadFieldPropertyType(propertyType))
{
FileUploadValue? originalValue = FileUploadValueParser.Parse(blockPropertyValue.Value);
if (string.IsNullOrWhiteSpace(originalValue?.Src))
{
continue;
}
paths.Add(MediaFileManager.FileSystem.GetRelativePath(originalValue.Src));
continue;
}
if (IsBlockListPropertyType(propertyType))
{
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockListEditorValues));
continue;
}
if (IsBlockGridPropertyType(propertyType))
{
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockGridEditorValues));
continue;
}
if (IsRichTextPropertyType(propertyType))
{
paths.AddRange(GetPathsFromRichTextPropertyValue(blockPropertyValue));
continue;
}
}
return paths;
}
private IReadOnlyCollection<string> GetPathsFromBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
BlockEditorData<TValue, TLayout>? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues);
return GetPathsFromBlockValue(blockItemEditorDataValue?.BlockValue);
}
private IReadOnlyCollection<string> GetPathsFromRichTextProperty(IProperty property)
{
var paths = new List<string>();
IPropertyValue? propertyValue = property.Values.FirstOrDefault();
if (propertyValue is null)
{
return paths;
}
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.PublishedValue)));
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.EditedValue)));
return paths;
}
private IReadOnlyCollection<string> GetPathsFromRichTextPropertyValue(BlockPropertyValue blockItemDataValue)
{
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockItemDataValue.Value);
// Ensure the property type is populated on all blocks.
richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache);
return GetPathsFromBlockValue(richTextEditorValue?.Blocks);
}
}

View File

@@ -1,218 +0,0 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Extensions;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
/// <summary>
/// Provides base class for notification handler that processes file uploads when a content entity is deleted, removing associated files.
/// </summary>
internal abstract class FileUploadEntityDeletedNotificationHandlerBase : FileUploadNotificationHandlerBase
{
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockListEditorValues;
private readonly BlockEditorValues<BlockGridValue, BlockGridLayoutItem> _blockGridEditorValues;
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadEntityDeletedNotificationHandlerBase"/> class.
/// </summary>
protected FileUploadEntityDeletedNotificationHandlerBase(
IJsonSerializer jsonSerializer,
MediaFileManager mediaFileManager,
IBlockEditorElementTypeCache elementTypeCache,
ILogger logger)
: base(jsonSerializer, mediaFileManager, elementTypeCache)
{
_blockListEditorValues = new(new BlockListEditorDataConverter(jsonSerializer), elementTypeCache, logger);
_blockGridEditorValues = new(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger);
}
/// <summary>
/// Deletes all file upload property files contained within a collection of content entities.
/// </summary>
/// <param name="deletedEntities"></param>
protected void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
{
IReadOnlyList<string> filePathsToDelete = ContainedFilePaths(deletedEntities);
MediaFileManager.DeleteMediaFiles(filePathsToDelete);
}
/// <summary>
/// Gets the paths to all file upload property files contained within a collection of content entities.
/// </summary>
private IReadOnlyList<string> ContainedFilePaths(IEnumerable<IContentBase> entities)
{
var paths = new List<string>();
foreach (IProperty? property in entities.SelectMany(x => x.Properties))
{
if (IsUploadFieldPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromUploadFieldProperty(property));
continue;
}
if (IsBlockListPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromBlockProperty(property, _blockListEditorValues));
continue;
}
if (IsBlockGridPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromBlockProperty(property, _blockGridEditorValues));
continue;
}
if (IsRichTextPropertyType(property.PropertyType))
{
paths.AddRange(GetPathsFromRichTextProperty(property));
continue;
}
}
return paths.Distinct().ToList().AsReadOnly();
}
private IEnumerable<string> GetPathsFromUploadFieldProperty(IProperty property)
{
foreach (IPropertyValue propertyValue in property.Values)
{
if (propertyValue.PublishedValue != null && propertyValue.PublishedValue is string publishedUrl && !string.IsNullOrWhiteSpace(publishedUrl))
{
yield return MediaFileManager.FileSystem.GetRelativePath(publishedUrl);
}
if (propertyValue.EditedValue != null && propertyValue.EditedValue is string editedUrl && !string.IsNullOrWhiteSpace(editedUrl))
{
yield return MediaFileManager.FileSystem.GetRelativePath(editedUrl);
}
}
}
private IReadOnlyCollection<string> GetPathsFromBlockProperty<TValue, TLayout>(IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
var paths = new List<string>();
foreach (IPropertyValue blockPropertyValue in property.Values)
{
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.PublishedValue, blockEditorValues)?.BlockValue));
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.EditedValue, blockEditorValues)?.BlockValue));
}
return paths;
}
private IReadOnlyCollection<string> GetPathsFromBlockValue(BlockValue? blockValue)
{
var paths = new List<string>();
if (blockValue is null)
{
return paths;
}
IEnumerable<BlockPropertyValue> blockPropertyValues = blockValue.ContentData
.Concat(blockValue.SettingsData)
.SelectMany(x => x.Values);
foreach (BlockPropertyValue blockPropertyValue in blockPropertyValues)
{
if (blockPropertyValue.Value == null)
{
continue;
}
IPropertyType? propertyType = blockPropertyValue.PropertyType;
if (propertyType == null)
{
continue;
}
if (IsUploadFieldPropertyType(propertyType))
{
FileUploadValue? originalValue = FileUploadValueParser.Parse(blockPropertyValue.Value);
if (string.IsNullOrWhiteSpace(originalValue?.Src))
{
continue;
}
paths.Add(MediaFileManager.FileSystem.GetRelativePath(originalValue.Src));
continue;
}
if (IsBlockListPropertyType(propertyType))
{
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockListEditorValues));
continue;
}
if (IsBlockGridPropertyType(propertyType))
{
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockGridEditorValues));
continue;
}
if (IsRichTextPropertyType(propertyType))
{
paths.AddRange(GetPathsFromRichTextPropertyValue(blockPropertyValue));
continue;
}
}
return paths;
}
private IReadOnlyCollection<string> GetPathsFromBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, BlockEditorValues<TValue, TLayout> blockEditorValues)
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
BlockEditorData<TValue, TLayout>? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues);
return GetPathsFromBlockValue(blockItemEditorDataValue?.BlockValue);
}
private IReadOnlyCollection<string> GetPathsFromRichTextProperty(IProperty property)
{
var paths = new List<string>();
IPropertyValue? propertyValue = property.Values.FirstOrDefault();
if (propertyValue is null)
{
return paths;
}
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.PublishedValue)));
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.EditedValue)));
return paths;
}
private IReadOnlyCollection<string> GetPathsFromRichTextPropertyValue(BlockPropertyValue blockItemDataValue)
{
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockItemDataValue.Value);
// Ensure the property type is populated on all blocks.
richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache);
return GetPathsFromBlockValue(richTextEditorValue?.Blocks);
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
/// <summary>
/// Implements a notification handler that processes file uploads when media is deleted, removing associated files.
/// </summary>
internal sealed class FileUploadMediaDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<MediaDeletedNotification>
{
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadMediaDeletedNotificationHandler"/> class.
/// </summary>
public FileUploadMediaDeletedNotificationHandler(
IJsonSerializer jsonSerializer,
MediaFileManager mediaFileManager,
IBlockEditorElementTypeCache elementTypeCache,
ILogger<FileUploadContentDeletedNotificationHandler> logger)
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
{
}
/// <inheritdoc/>
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
}

View File

@@ -1,29 +0,0 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
/// <summary>
/// Implements a notification handler that processes file uploads when a member is deleted, removing associated files.
/// </summary>
internal sealed class FileUploadMemberDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<MemberDeletedNotification>
{
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadMemberDeletedNotificationHandler"/> class.
/// </summary>
public FileUploadMemberDeletedNotificationHandler(
IJsonSerializer jsonSerializer,
MediaFileManager mediaFileManager,
IBlockEditorElementTypeCache elementTypeCache,
ILogger<FileUploadContentDeletedNotificationHandler> logger)
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
{
}
/// <inheritdoc/>
public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
}

View File

@@ -96,7 +96,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
blueprint.SetValue("keywords", "blueprint 3");
blueprint.SetValue("description", "blueprint 4");
ContentService.SaveBlueprint(blueprint);
ContentService.SaveBlueprint(blueprint, null);
var found = ContentService.GetBlueprintsForContentTypes().ToArray();
Assert.AreEqual(1, found.Length);
@@ -121,7 +121,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
blueprint.SetValue("keywords", "blueprint 3");
blueprint.SetValue("description", "blueprint 4");
ContentService.SaveBlueprint(blueprint);
ContentService.SaveBlueprint(blueprint, null);
ContentService.DeleteBlueprint(blueprint);
@@ -148,7 +148,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
ContentService.Save(originalPage);
var fromContent = ContentService.CreateBlueprintFromContent(originalPage, "hello world");
ContentService.SaveBlueprint(fromContent);
ContentService.SaveBlueprint(fromContent, originalPage);
Assert.IsTrue(fromContent.HasIdentity);
Assert.AreEqual("blueprint 1", fromContent.Properties["title"]?.GetValue());
@@ -176,7 +176,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent
{
var blueprint =
ContentBuilder.CreateTextpageContent(i % 2 == 0 ? ct1 : ct2, "hello" + i, Constants.System.Root);
ContentService.SaveBlueprint(blueprint);
ContentService.SaveBlueprint(blueprint, null);
}
var found = ContentService.GetBlueprintsForContentTypes().ToArray();

View File

@@ -116,7 +116,7 @@ internal sealed class TelemetryProviderTests : UmbracoIntegrationTest
blueprint.SetValue("keywords", "blueprint 3");
blueprint.SetValue("description", "blueprint 4");
ContentService.SaveBlueprint(blueprint);
ContentService.SaveBlueprint(blueprint, null);
var fromBlueprint = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Key);
Assert.IsNotNull(fromBlueprint);