From b5195ed8eb40acf9c74ebd5aadcd837d0aa34c86 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 7 Jul 2025 14:15:17 +0200 Subject: [PATCH] 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. --- .../ContentSavedBlueprintNotification.cs | 14 ++ .../ContentBlueprintEditingService.cs | 6 +- src/Umbraco.Core/Services/ContentService.cs | 5 +- src/Umbraco.Core/Services/IContentService.cs | 9 + .../UmbracoBuilder.CoreServices.cs | 9 +- .../MoveDocumentBlueprintsToFolders.cs | 4 +- .../FileUploadPropertyValueEditor.cs | 7 +- ...tCopiedOrScaffoldedNotificationHandler.cs} | 101 ++++---- ...UploadContentDeletedNotificationHandler.cs | 213 ++++++++++++++++- ...oadEntityDeletedNotificationHandlerBase.cs | 218 ------------------ ...leUploadMediaDeletedNotificationHandler.cs | 29 --- ...eUploadMemberDeletedNotificationHandler.cs | 29 --- .../Services/ContentServiceTests.cs | 8 +- .../Services/TelemetryProviderTests.cs | 2 +- 14 files changed, 314 insertions(+), 340 deletions(-) rename src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/{FileUploadContentCopiedNotificationHandler.cs => FileUploadContentCopiedOrScaffoldedNotificationHandler.cs} (67%) delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadEntityDeletedNotificationHandlerBase.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMediaDeletedNotificationHandler.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMemberDeletedNotificationHandler.cs diff --git a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs index 9219b89f23..897065a532 100644 --- a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications; + /// /// A notification that is used to trigger the IContentService when the SavedBlueprint method is called in the API. /// @@ -14,8 +15,21 @@ public sealed class ContentSavedBlueprintNotification : ObjectNotification /// Getting the saved blueprint object. /// public IContent SavedBlueprint => Target; + + /// + /// Getting the saved blueprint object. + /// + public IContent? CreatedFromContent { get; } + } diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index 5e8357e453..d2e68ad327 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -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) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 34ac8db8ff..0c3b61a1c3 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -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(); diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 423f157874..a7bde2dc46 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -47,8 +47,17 @@ public interface IContentService : IContentServiceBase /// /// Saves a blueprint. /// + [Obsolete("Please use the method taking all parameters. Scheduled for removal in Umbraco 18.")] void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId); + /// + /// Saves a blueprint. + /// + 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 + /// /// Deletes a blueprint. /// diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index d0c68d5aa5..66b3687cc2 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -356,11 +356,14 @@ public static partial class UmbracoBuilderExtensions .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs index d09783c061..2c48b1e93b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs @@ -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); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs index 22c7402b7b..241c817cec 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -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 diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedOrScaffoldedNotificationHandler.cs similarity index 67% rename from src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedNotificationHandler.cs rename to src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedOrScaffoldedNotificationHandler.cs index b056d31c79..2222b65b3f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentCopiedOrScaffoldedNotificationHandler.cs @@ -14,24 +14,26 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers; /// -/// 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. /// -internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNotificationHandlerBase, INotificationHandler +internal sealed class FileUploadContentCopiedOrScaffoldedNotificationHandler : FileUploadNotificationHandlerBase, + INotificationHandler, + INotificationHandler, + INotificationHandler { private readonly IContentService _contentService; - private readonly BlockEditorValues _blockListEditorValues; private readonly BlockEditorValues _blockGridEditorValues; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public FileUploadContentCopiedNotificationHandler( + public FileUploadContentCopiedOrScaffoldedNotificationHandler( IJsonSerializer jsonSerializer, MediaFileManager mediaFileManager, IBlockEditorElementTypeCache elementTypeCache, - ILogger logger, + ILogger logger, IContentService contentService) : base(jsonSerializer, mediaFileManager, elementTypeCache) { @@ -41,51 +43,66 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot } /// - public void Handle(ContentCopiedNotification notification) - { - ArgumentNullException.ThrowIfNull(notification); + public void Handle(ContentCopiedNotification notification) => Handle(notification.Original, notification.Copy, (IContent c) => _contentService.Save(c)); + /// + public void Handle(ContentScaffoldedNotification notification) => Handle(notification.Original, notification.Scaffold); + + /// + 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? 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(ContentCopiedNotification notification, IProperty property, BlockEditorValues blockEditorValues) + private bool UpdateBlockProperty(IContent content, IProperty property, BlockEditorValues blockEditorValues) where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { @@ -120,11 +137,11 @@ internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNot BlockEditorData? 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(ContentCopiedNotification notification, BlockEditorData? blockEditorData) + private (bool, string?) UpdateBlockEditorData(IContent content, BlockEditorData? blockEditorData) where TValue : BlockValue, 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(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 blockPropertyValues) + private bool UpdateBlockPropertyValues(IContent content, bool isUpdated, IEnumerable 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(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification, BlockEditorValues blockEditorValues) + private (bool, string?) UpdateBlockPropertyValue(BlockPropertyValue blockItemDataValue, IContent content, BlockEditorValues blockEditorValues) where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { BlockEditorData? 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) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentDeletedNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentDeletedNotificationHandler.cs index 681c31cc58..4203209b58 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentDeletedNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadContentDeletedNotificationHandler.cs @@ -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; /// -/// 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. /// -internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler +internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadNotificationHandlerBase, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private readonly BlockEditorValues _blockListEditorValues; + private readonly BlockEditorValues _blockGridEditorValues; + /// /// Initializes a new instance of the class. /// @@ -20,10 +33,204 @@ internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadEn MediaFileManager mediaFileManager, IBlockEditorElementTypeCache elementTypeCache, ILogger logger) - : base(jsonSerializer, mediaFileManager, elementTypeCache, logger) + : base(jsonSerializer, mediaFileManager, elementTypeCache) { + _blockListEditorValues = new(new BlockListEditorDataConverter(jsonSerializer), elementTypeCache, logger); + _blockGridEditorValues = new(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); } /// public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + + /// + public void Handle(ContentDeletedBlueprintNotification notification) => DeleteContainedFiles(notification.DeletedBlueprints); + + /// + public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + + /// + public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + + /// + /// Deletes all file upload property files contained within a collection of content entities. + /// + /// + private void DeleteContainedFiles(IEnumerable deletedEntities) + { + IReadOnlyList filePathsToDelete = ContainedFilePaths(deletedEntities); + MediaFileManager.DeleteMediaFiles(filePathsToDelete); + } + + /// + /// Gets the paths to all file upload property files contained within a collection of content entities. + /// + private IReadOnlyList ContainedFilePaths(IEnumerable entities) + { + var paths = new List(); + + 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 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 GetPathsFromBlockProperty(IProperty property, BlockEditorValues blockEditorValues) + where TValue : BlockValue, new() + where TLayout : class, IBlockLayoutItem, new() + { + var paths = new List(); + + 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 GetPathsFromBlockValue(BlockValue? blockValue) + { + var paths = new List(); + + if (blockValue is null) + { + return paths; + } + + IEnumerable 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 GetPathsFromBlockPropertyValue(BlockPropertyValue blockItemDataValue, BlockEditorValues blockEditorValues) + where TValue : BlockValue, new() + where TLayout : class, IBlockLayoutItem, new() + { + BlockEditorData? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues); + + return GetPathsFromBlockValue(blockItemEditorDataValue?.BlockValue); + } + + private IReadOnlyCollection GetPathsFromRichTextProperty(IProperty property) + { + var paths = new List(); + + 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 GetPathsFromRichTextPropertyValue(BlockPropertyValue blockItemDataValue) + { + RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockItemDataValue.Value); + + // Ensure the property type is populated on all blocks. + richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache); + + return GetPathsFromBlockValue(richTextEditorValue?.Blocks); + } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadEntityDeletedNotificationHandlerBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadEntityDeletedNotificationHandlerBase.cs deleted file mode 100644 index 40877d2fef..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadEntityDeletedNotificationHandlerBase.cs +++ /dev/null @@ -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; - -/// -/// Provides base class for notification handler that processes file uploads when a content entity is deleted, removing associated files. -/// -internal abstract class FileUploadEntityDeletedNotificationHandlerBase : FileUploadNotificationHandlerBase -{ - private readonly BlockEditorValues _blockListEditorValues; - private readonly BlockEditorValues _blockGridEditorValues; - - /// - /// Initializes a new instance of the class. - /// - 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); - } - - /// - /// Deletes all file upload property files contained within a collection of content entities. - /// - /// - protected void DeleteContainedFiles(IEnumerable deletedEntities) - { - IReadOnlyList filePathsToDelete = ContainedFilePaths(deletedEntities); - MediaFileManager.DeleteMediaFiles(filePathsToDelete); - } - - /// - /// Gets the paths to all file upload property files contained within a collection of content entities. - /// - private IReadOnlyList ContainedFilePaths(IEnumerable entities) - { - var paths = new List(); - - 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 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 GetPathsFromBlockProperty(IProperty property, BlockEditorValues blockEditorValues) - where TValue : BlockValue, new() - where TLayout : class, IBlockLayoutItem, new() - { - var paths = new List(); - - 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 GetPathsFromBlockValue(BlockValue? blockValue) - { - var paths = new List(); - - if (blockValue is null) - { - return paths; - } - - IEnumerable 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 GetPathsFromBlockPropertyValue(BlockPropertyValue blockItemDataValue, BlockEditorValues blockEditorValues) - where TValue : BlockValue, new() - where TLayout : class, IBlockLayoutItem, new() - { - BlockEditorData? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues); - - return GetPathsFromBlockValue(blockItemEditorDataValue?.BlockValue); - } - - private IReadOnlyCollection GetPathsFromRichTextProperty(IProperty property) - { - var paths = new List(); - - 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 GetPathsFromRichTextPropertyValue(BlockPropertyValue blockItemDataValue) - { - RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockItemDataValue.Value); - - // Ensure the property type is populated on all blocks. - richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache); - - return GetPathsFromBlockValue(richTextEditorValue?.Blocks); - } -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMediaDeletedNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMediaDeletedNotificationHandler.cs deleted file mode 100644 index 3a07193ec8..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMediaDeletedNotificationHandler.cs +++ /dev/null @@ -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; - -/// -/// Implements a notification handler that processes file uploads when media is deleted, removing associated files. -/// -internal sealed class FileUploadMediaDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler -{ - /// - /// Initializes a new instance of the class. - /// - public FileUploadMediaDeletedNotificationHandler( - IJsonSerializer jsonSerializer, - MediaFileManager mediaFileManager, - IBlockEditorElementTypeCache elementTypeCache, - ILogger logger) - : base(jsonSerializer, mediaFileManager, elementTypeCache, logger) - { - } - - /// - public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMemberDeletedNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMemberDeletedNotificationHandler.cs deleted file mode 100644 index 91433b88b9..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/NotificationHandlers/FileUploadMemberDeletedNotificationHandler.cs +++ /dev/null @@ -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; - -/// -/// Implements a notification handler that processes file uploads when a member is deleted, removing associated files. -/// -internal sealed class FileUploadMemberDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler -{ - /// - /// Initializes a new instance of the class. - /// - public FileUploadMemberDeletedNotificationHandler( - IJsonSerializer jsonSerializer, - MediaFileManager mediaFileManager, - IBlockEditorElementTypeCache elementTypeCache, - ILogger logger) - : base(jsonSerializer, mediaFileManager, elementTypeCache, logger) - { - } - - /// - public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); -} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 2c1ea1e278..3d04a9fa6c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -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(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs index c5d6548677..8cce10b716 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs @@ -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);