diff --git a/src/Umbraco.Core/Cache/ValueEditorCache.cs b/src/Umbraco.Core/Cache/ValueEditorCache.cs index 358134ab14f3..4347047c6b4b 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCache.cs @@ -51,7 +51,15 @@ public void ClearCache(IEnumerable dataTypeIds) { foreach (Dictionary editors in _valueEditorCache.Values) { - editors.Remove(id); + if (editors.TryGetValue(id, out IDataValueEditor? editor)) + { + if (editor is IDisposable disposable) + { + disposable.Dispose(); + } + + editors.Remove(id); + } } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs index 34a4d33fd913..5478498d2f7c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Editors; @@ -16,11 +17,18 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// /// The value editor for the file upload property editor. /// -internal class FileUploadPropertyValueEditor : DataValueEditor +/// +/// As this class is loaded into which can be cleared, it needs +/// to be disposable in order to properly clean up resources such as +/// the settings change subscription and avoid a memory leak. +/// +internal class FileUploadPropertyValueEditor : DataValueEditor, IDisposable { private readonly MediaFileManager _mediaFileManager; private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator; + private ContentSettings _contentSettings; + private readonly IDisposable? _contentSettingsChangeSubscription; public FileUploadPropertyValueEditor( DataEditorAttribute attribute, @@ -36,7 +44,7 @@ public FileUploadPropertyValueEditor( _mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager)); _fileStreamSecurityValidator = fileStreamSecurityValidator; _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); - contentSettings.OnChange(x => _contentSettings = x); + _contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x); } /// @@ -163,4 +171,6 @@ public FileUploadPropertyValueEditor( return filepath; } + + public void Dispose() => _contentSettingsChangeSubscription?.Dispose(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index 0be7cd4f72bb..bb4202fb0b10 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -21,6 +21,10 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// /// Represents an image cropper property editor. /// +/// +/// As this class is not registered with DI as a singleton, it must be disposed to release +/// the settings change subscription and avoid a memory leak. +/// [DataEditor( Constants.PropertyEditors.Aliases.ImageCropper, "Image Cropper", @@ -42,6 +46,7 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, private readonly IIOHelper _ioHelper; private readonly ILogger _logger; private readonly MediaFileManager _mediaFileManager; + private ContentSettings _contentSettings; // Scheduled for removal in v12 diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index 1c0567bce24e..4080c1caefe4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -23,13 +23,20 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// /// The value editor for the image cropper property editor. /// -internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core vs web? +/// +/// As this class is loaded into which can be cleared, it needs +/// to be disposable in order to properly clean up resources such as +/// the settings change subscription and avoid a memory leak. +/// +internal class ImageCropperPropertyValueEditor : DataValueEditor, IDisposable { private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache; private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator; private readonly ILogger _logger; private readonly MediaFileManager _mediaFileManager; + private ContentSettings _contentSettings; + private readonly IDisposable? _contentSettingsChangeSubscription; public ImageCropperPropertyValueEditor( DataEditorAttribute attribute, @@ -49,7 +56,7 @@ public ImageCropperPropertyValueEditor( _contentSettings = contentSettings.CurrentValue; _dataTypeConfigurationCache = dataTypeConfigurationCache; _fileStreamSecurityValidator = fileStreamSecurityValidator; - contentSettings.OnChange(x => _contentSettings = x); + _contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x); } /// @@ -252,4 +259,6 @@ public override string ConvertDbToString(IPropertyType propertyType, object? val return filepath; } + + public void Dispose() => _contentSettingsChangeSubscription?.Dispose(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs index 847ac9cff5ca..51a120c29ea0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs @@ -11,10 +11,19 @@ namespace Umbraco.Cms.Core.PropertyEditors; -internal class UploadFileTypeValidator : IValueValidator +/// +/// The value editor for file upload property editors. +/// +/// +/// As this class is not registered with DI as a singleton, it must be disposed to release +/// the settings change subscription and avoid a memory leak. +/// +internal class UploadFileTypeValidator : IValueValidator, IDisposable { private readonly ILocalizedTextService _localizedTextService; + private ContentSettings _contentSettings; + private readonly IDisposable? _contentSettingsChangeSubscription; public UploadFileTypeValidator( ILocalizedTextService localizedTextService, @@ -23,7 +32,7 @@ public UploadFileTypeValidator( _localizedTextService = localizedTextService; _contentSettings = contentSettings.CurrentValue; - contentSettings.OnChange(x => _contentSettings = x); + _contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x); } public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) @@ -103,4 +112,6 @@ internal static bool TryGetFileExtension(string? fileName, [MaybeNullWhen(false) extension = fileName.GetFileExtension().TrimStart("."); return true; } + + public void Dispose() => _contentSettingsChangeSubscription?.Dispose(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index c8785d20538e..7709c42d8768 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -31,8 +31,12 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; /// A value converter for TinyMCE that will ensure any macro content is rendered properly even when /// used dynamically. /// +/// +/// As this class is not registered with DI as a singleton, it must be disposed to release +/// the settings change subscription and avoid a memory leak. +/// [DefaultPropertyValueConverter] -public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter +public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter, IDisposable { private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _linkParser; @@ -47,7 +51,9 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel private readonly ILogger _logger; private readonly IApiElementBuilder _apiElementBuilder; private readonly RichTextBlockPropertyValueConstructorCache _constructorCache; + private DeliveryApiSettings _deliveryApiSettings; + private readonly IDisposable? _deliveryApiSettingsChangeSubscription; [Obsolete("Please use the constructor that takes all arguments. Will be removed in V14.")] public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, @@ -107,8 +113,9 @@ public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAcc _apiElementBuilder = apiElementBuilder; _constructorCache = constructorCache; _logger = logger; + _deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue; - deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); + _deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); } public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => @@ -314,4 +321,6 @@ private class RichTextEditorIntermediateValue : IRichTextEditorIntermediateValue public required RichTextBlockModel? RichTextBlockModel { get; set; } } + + public void Dispose() => _deliveryApiSettingsChangeSubscription?.Dispose(); } diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.DeliveryApi.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.DeliveryApi.cs index 1750310e7159..40c671513749 100644 --- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.DeliveryApi.cs +++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.DeliveryApi.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; @@ -17,7 +17,7 @@ internal sealed class DeliveryApiContentIndexingNotificationHandler : { private readonly IDeliveryApiIndexingHandler _deliveryApiIndexingHandler; private readonly ILogger _logger; - private DeliveryApiSettings _deliveryApiSettings; + private readonly DeliveryApiSettings _deliveryApiSettings; public DeliveryApiContentIndexingNotificationHandler( IDeliveryApiIndexingHandler deliveryApiIndexingHandler, @@ -27,7 +27,6 @@ public DeliveryApiContentIndexingNotificationHandler( _deliveryApiIndexingHandler = deliveryApiIndexingHandler; _logger = logger; _deliveryApiSettings = deliveryApiSettings.CurrentValue; - deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); } public void Handle(ContentCacheRefresherNotification notification) diff --git a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index da3ac9013f2f..eb47f4acd33b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -6,7 +6,6 @@ using Newtonsoft.Json; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.Attributes; namespace Umbraco.Cms.Web.BackOffice.Controllers; @@ -16,7 +15,8 @@ public class HelpController : UmbracoAuthorizedJsonController { private static HttpClient? _httpClient; private readonly ILogger _logger; - private HelpPageSettings? _helpPageSettings; + + private readonly HelpPageSettings? _helpPageSettings; [ActivatorUtilitiesConstructor] public HelpController( @@ -25,12 +25,9 @@ public HelpController( { _logger = logger; - ResetHelpPageSettings(helpPageSettings.CurrentValue); - helpPageSettings.OnChange(ResetHelpPageSettings); + _helpPageSettings = helpPageSettings.CurrentValue; } - private void ResetHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings; - public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index b70661c0af6e..bcdc9ac713e6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -23,7 +23,7 @@ public class ImagesController : UmbracoAuthorizedApiController { private readonly MediaFileManager _mediaFileManager; private readonly IImageUrlGenerator _imageUrlGenerator; - private ContentSettings _contentSettings; + private readonly ContentSettings _contentSettings; [Obsolete("Use non obsolete-constructor. Scheduled for removal in Umbraco 13.")] public ImagesController( @@ -45,8 +45,6 @@ public ImagesController( _mediaFileManager = mediaFileManager; _imageUrlGenerator = imageUrlGenerator; _contentSettings = contentSettingsMonitor.CurrentValue; - - contentSettingsMonitor.OnChange(x => _contentSettings = x); } ///