diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 2ec4a72480..f761007146 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Media.EmbedProviders; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.ServerEvents; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Webhooks; @@ -92,6 +93,7 @@ public static partial class UmbracoBuilderExtensions builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.WebhookEvents().AddCms(true); + builder.ContentTypeFilters(); } /// @@ -246,4 +248,11 @@ public static partial class UmbracoBuilderExtensions /// public static ContentIndexHandlerCollectionBuilder ContentIndexHandlers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + /// + /// Gets the content type filters collection builder. + /// + /// The builder. + public static ContentTypeFilterCollectionBuilder ContentTypeFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 78692dff98..b68a1685ac 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -46,6 +46,7 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; +using Umbraco.Cms.Core.Services.Filters; namespace Umbraco.Cms.Core.DependencyInjection { @@ -444,7 +445,6 @@ namespace Umbraco.Cms.Core.DependencyInjection // Routing Services.AddUnique(); Services.AddNotificationAsyncHandler(); - } } } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index f12daff4f2..3566696af4 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -4,12 +4,11 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.Locking; -using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -28,7 +27,8 @@ public class ContentTypeService : ContentTypeServiceBase + userIdKeyResolver, + contentTypeFilters) => ContentService = contentService; [Obsolete("Use the ctor specifying all dependencies instead")] @@ -65,6 +66,32 @@ public class ContentTypeService : ContentTypeServiceBase()) { } + [Obsolete("Use the ctor specifying all dependencies instead")] + public ContentTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IContentService contentService, + IContentTypeRepository repository, + IAuditRepository auditRepository, + IDocumentTypeContainerRepository entityContainerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + contentService, + repository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { } + protected override int[] ReadLockIds => ContentTypeLocks.ReadLockIds; protected override int[] WriteLockIds => ContentTypeLocks.WriteLockIds; diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 74fe09f8c8..4e26aee16c 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; @@ -25,6 +26,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe private readonly IEntityRepository _entityRepository; private readonly IEventAggregator _eventAggregator; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly ContentTypeFilterCollection _contentTypeFilters; protected ContentTypeServiceBase( ICoreScopeProvider provider, @@ -35,7 +37,8 @@ public abstract class ContentTypeServiceBase : ContentTypeSe IEntityContainerRepository containerRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator, - IUserIdKeyResolver userIdKeyResolver) + IUserIdKeyResolver userIdKeyResolver, + ContentTypeFilterCollection contentTypeFilters) : base(provider, loggerFactory, eventMessagesFactory) { Repository = repository; @@ -44,6 +47,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe _entityRepository = entityRepository; _eventAggregator = eventAggregator; _userIdKeyResolver = userIdKeyResolver; + _contentTypeFilters = contentTypeFilters; } [Obsolete("Use the ctor specifying all dependencies instead")] @@ -69,6 +73,31 @@ public abstract class ContentTypeServiceBase : ContentTypeSe { } + [Obsolete("Use the ctor specifying all dependencies instead")] + protected ContentTypeServiceBase( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + TRepository repository, + IAuditRepository auditRepository, + IEntityContainerRepository containerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + repository, + auditRepository, + containerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + protected TRepository Repository { get; } protected abstract int[] WriteLockIds { get; } protected abstract int[] ReadLockIds { get; } @@ -1129,7 +1158,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe #region Allowed types /// - public Task> GetAllAllowedAsRootAsync(int skip, int take) + public async Task> GetAllAllowedAsRootAsync(int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); @@ -1139,28 +1168,39 @@ public abstract class ContentTypeServiceBase : ContentTypeSe IQuery query = ScopeProvider.CreateQuery().Where(x => x.AllowedAsRoot); IEnumerable contentTypes = Repository.Get(query).ToArray(); + foreach (IContentTypeFilter filter in _contentTypeFilters) + { + contentTypes = await filter.FilterAllowedAtRootAsync(contentTypes); + } + var pagedModel = new PagedModel { Total = contentTypes.Count(), Items = contentTypes.Skip(skip).Take(take) }; - return Task.FromResult(pagedModel); + return pagedModel; } /// - public Task?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take) + public async Task?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); TItem? parent = Get(key); if (parent?.AllowedContentTypes is null) { - return Task.FromResult(Attempt.FailWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null)); + return Attempt.FailWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null); + } + + IEnumerable allowedContentTypes = parent.AllowedContentTypes; + foreach (IContentTypeFilter filter in _contentTypeFilters) + { + allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key); } PagedModel result; - if (parent.AllowedContentTypes.Any() is false) + if (allowedContentTypes.Any() is false) { // no content types allowed under parent result = new PagedModel @@ -1173,7 +1213,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe { // Get the sorted keys. Whilst we can't guarantee the order that comes back from GetMany, we can use // this to sort the resulting list of allowed children. - Guid[] sortedKeys = parent.AllowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray(); + Guid[] sortedKeys = allowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray(); TItem[] allowedChildren = GetMany(sortedKeys).ToArray(); result = new PagedModel @@ -1183,7 +1223,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe }; } - return Task.FromResult(Attempt.SucceedWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result)); + return Attempt.SucceedWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result); } #endregion diff --git a/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs new file mode 100644 index 0000000000..50d6757b5f --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Defines an ordered collection of . +/// +public class ContentTypeFilterCollection : BuilderCollectionBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The collection items. + public ContentTypeFilterCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs new file mode 100644 index 0000000000..f1323543a9 --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Builds an ordered collection of . +/// +public class ContentTypeFilterCollectionBuilder : OrderedCollectionBuilderBase +{ + /// + protected override ContentTypeFilterCollectionBuilder This => this; +} diff --git a/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs b/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs new file mode 100644 index 0000000000..e0f582723c --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Defines methods for filtering content types after retrieval from the database. +/// +public interface IContentTypeFilter +{ + /// + /// Filters the content types retrieved for being allowed at the root. + /// + /// Retrieved collection of content types. + /// Filtered collection of content types. + Task> FilterAllowedAtRootAsync(IEnumerable contentTypes) + where TItem : IContentTypeComposition; + + /// + /// Filters the content types retrieved for being allowed as children of a parent content type. + /// + /// Retrieved collection of content types. + /// The parent content type key. + /// Filtered collection of content types. + Task> FilterAllowedChildrenAsync(IEnumerable contentTypes, Guid parentKey); +} diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs index 359b4a99a9..e6dcf15939 100644 --- a/src/Umbraco.Core/Services/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.Locking; using Umbraco.Extensions; @@ -24,7 +25,8 @@ public class MediaTypeService : ContentTypeServiceBase MediaService = mediaService; + userIdKeyResolver, + contentTypeFilters) => MediaService = mediaService; [Obsolete("Use the constructor with all dependencies instead")] public MediaTypeService( @@ -61,6 +64,32 @@ public class MediaTypeService : ContentTypeServiceBase()) + { + } protected override int[] ReadLockIds => MediaTypeLocks.ReadLockIds; diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index 72f4cde792..f2fa986fde 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -15,6 +16,34 @@ public class MemberTypeService : ContentTypeServiceBase()) { - MemberService = memberService; - _memberTypeRepository = memberTypeRepository; } // beware! order is important to avoid deadlocks