Skip to content

Commit 171ada2

Browse files
authored
Allow for filtering of document type allowed children and allowed at root when creating new content. (#18029)
* Creates IContentTypeFilterService with a "no-op" implementation for filtering the content types available for selection at root and as children. * Rework to collection so packages and implementors can stack filters if they need to.
1 parent ef478cf commit 171ada2

9 files changed

+209
-19
lines changed

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Umbraco.Cms.Core.PropertyEditors;
1313
using Umbraco.Cms.Core.Routing;
1414
using Umbraco.Cms.Core.ServerEvents;
15+
using Umbraco.Cms.Core.Services.Filters;
1516
using Umbraco.Cms.Core.Snippets;
1617
using Umbraco.Cms.Core.Strings;
1718
using Umbraco.Cms.Core.Webhooks;
@@ -92,6 +93,7 @@ internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder)
9293
builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes<ISortHandler>());
9394
builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes<IContentIndexHandler>());
9495
builder.WebhookEvents().AddCms(true);
96+
builder.ContentTypeFilters();
9597
}
9698

9799
/// <summary>
@@ -246,4 +248,11 @@ public static SortHandlerCollectionBuilder SortHandlers(this IUmbracoBuilder bui
246248
/// </summary>
247249
public static ContentIndexHandlerCollectionBuilder ContentIndexHandlers(this IUmbracoBuilder builder)
248250
=> builder.WithCollectionBuilder<ContentIndexHandlerCollectionBuilder>();
251+
252+
/// <summary>
253+
/// Gets the content type filters collection builder.
254+
/// </summary>
255+
/// <param name="builder">The builder.</param>
256+
public static ContentTypeFilterCollectionBuilder ContentTypeFilters(this IUmbracoBuilder builder)
257+
=> builder.WithCollectionBuilder<ContentTypeFilterCollectionBuilder>();
249258
}

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
using Umbraco.Cms.Core.Templates;
4747
using Umbraco.Cms.Core.Web;
4848
using Umbraco.Extensions;
49+
using Umbraco.Cms.Core.Services.Filters;
4950

5051
namespace Umbraco.Cms.Core.DependencyInjection
5152
{
@@ -444,7 +445,6 @@ private void AddCoreServices()
444445
// Routing
445446
Services.AddUnique<IDocumentUrlService, DocumentUrlService>();
446447
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlServiceInitializerNotificationHandler>();
447-
448448
}
449449
}
450450
}

src/Umbraco.Core/Services/ContentTypeService.cs

+31-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
using Umbraco.Cms.Core.Events;
55
using Umbraco.Cms.Core.Models;
66
using Umbraco.Cms.Core.Notifications;
7-
using Umbraco.Cms.Core.Persistence.Querying;
87
using Umbraco.Cms.Core.Persistence.Repositories;
98
using Umbraco.Cms.Core.Scoping;
109
using Umbraco.Cms.Core.Services.Changes;
10+
using Umbraco.Cms.Core.Services.Filters;
1111
using Umbraco.Cms.Core.Services.Locking;
12-
using Umbraco.Cms.Core.Services.OperationStatus;
1312

1413
namespace Umbraco.Cms.Core.Services;
1514

@@ -28,7 +27,8 @@ public ContentTypeService(
2827
IDocumentTypeContainerRepository entityContainerRepository,
2928
IEntityRepository entityRepository,
3029
IEventAggregator eventAggregator,
31-
IUserIdKeyResolver userIdKeyResolver)
30+
IUserIdKeyResolver userIdKeyResolver,
31+
ContentTypeFilterCollection contentTypeFilters)
3232
: base(
3333
provider,
3434
loggerFactory,
@@ -38,7 +38,8 @@ public ContentTypeService(
3838
entityContainerRepository,
3939
entityRepository,
4040
eventAggregator,
41-
userIdKeyResolver) =>
41+
userIdKeyResolver,
42+
contentTypeFilters) =>
4243
ContentService = contentService;
4344

4445
[Obsolete("Use the ctor specifying all dependencies instead")]
@@ -65,6 +66,32 @@ public ContentTypeService(
6566
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>())
6667
{ }
6768

69+
[Obsolete("Use the ctor specifying all dependencies instead")]
70+
public ContentTypeService(
71+
ICoreScopeProvider provider,
72+
ILoggerFactory loggerFactory,
73+
IEventMessagesFactory eventMessagesFactory,
74+
IContentService contentService,
75+
IContentTypeRepository repository,
76+
IAuditRepository auditRepository,
77+
IDocumentTypeContainerRepository entityContainerRepository,
78+
IEntityRepository entityRepository,
79+
IEventAggregator eventAggregator,
80+
IUserIdKeyResolver userIdKeyResolver)
81+
: this(
82+
provider,
83+
loggerFactory,
84+
eventMessagesFactory,
85+
contentService,
86+
repository,
87+
auditRepository,
88+
entityContainerRepository,
89+
entityRepository,
90+
eventAggregator,
91+
userIdKeyResolver,
92+
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
93+
{ }
94+
6895
protected override int[] ReadLockIds => ContentTypeLocks.ReadLockIds;
6996

7097
protected override int[] WriteLockIds => ContentTypeLocks.WriteLockIds;

src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs

+48-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Umbraco.Cms.Core.Persistence.Repositories;
1212
using Umbraco.Cms.Core.Scoping;
1313
using Umbraco.Cms.Core.Services.Changes;
14+
using Umbraco.Cms.Core.Services.Filters;
1415
using Umbraco.Cms.Core.Services.OperationStatus;
1516
using Umbraco.Extensions;
1617

@@ -25,6 +26,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
2526
private readonly IEntityRepository _entityRepository;
2627
private readonly IEventAggregator _eventAggregator;
2728
private readonly IUserIdKeyResolver _userIdKeyResolver;
29+
private readonly ContentTypeFilterCollection _contentTypeFilters;
2830

2931
protected ContentTypeServiceBase(
3032
ICoreScopeProvider provider,
@@ -35,7 +37,8 @@ protected ContentTypeServiceBase(
3537
IEntityContainerRepository containerRepository,
3638
IEntityRepository entityRepository,
3739
IEventAggregator eventAggregator,
38-
IUserIdKeyResolver userIdKeyResolver)
40+
IUserIdKeyResolver userIdKeyResolver,
41+
ContentTypeFilterCollection contentTypeFilters)
3942
: base(provider, loggerFactory, eventMessagesFactory)
4043
{
4144
Repository = repository;
@@ -44,6 +47,7 @@ protected ContentTypeServiceBase(
4447
_entityRepository = entityRepository;
4548
_eventAggregator = eventAggregator;
4649
_userIdKeyResolver = userIdKeyResolver;
50+
_contentTypeFilters = contentTypeFilters;
4751
}
4852

4953
[Obsolete("Use the ctor specifying all dependencies instead")]
@@ -69,6 +73,31 @@ protected ContentTypeServiceBase(
6973
{
7074
}
7175

76+
[Obsolete("Use the ctor specifying all dependencies instead")]
77+
protected ContentTypeServiceBase(
78+
ICoreScopeProvider provider,
79+
ILoggerFactory loggerFactory,
80+
IEventMessagesFactory eventMessagesFactory,
81+
TRepository repository,
82+
IAuditRepository auditRepository,
83+
IEntityContainerRepository containerRepository,
84+
IEntityRepository entityRepository,
85+
IEventAggregator eventAggregator,
86+
IUserIdKeyResolver userIdKeyResolver)
87+
: this(
88+
provider,
89+
loggerFactory,
90+
eventMessagesFactory,
91+
repository,
92+
auditRepository,
93+
containerRepository,
94+
entityRepository,
95+
eventAggregator,
96+
userIdKeyResolver,
97+
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
98+
{
99+
}
100+
72101
protected TRepository Repository { get; }
73102
protected abstract int[] WriteLockIds { get; }
74103
protected abstract int[] ReadLockIds { get; }
@@ -1129,7 +1158,7 @@ public TItem Copy(TItem original, string alias, string name, TItem? parent)
11291158
#region Allowed types
11301159

11311160
/// <inheritdoc />
1132-
public Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
1161+
public async Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
11331162
{
11341163
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
11351164

@@ -1139,28 +1168,39 @@ public Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
11391168
IQuery<TItem> query = ScopeProvider.CreateQuery<TItem>().Where(x => x.AllowedAsRoot);
11401169
IEnumerable<TItem> contentTypes = Repository.Get(query).ToArray();
11411170

1171+
foreach (IContentTypeFilter filter in _contentTypeFilters)
1172+
{
1173+
contentTypes = await filter.FilterAllowedAtRootAsync(contentTypes);
1174+
}
1175+
11421176
var pagedModel = new PagedModel<TItem>
11431177
{
11441178
Total = contentTypes.Count(),
11451179
Items = contentTypes.Skip(skip).Take(take)
11461180
};
11471181

1148-
return Task.FromResult(pagedModel);
1182+
return pagedModel;
11491183
}
11501184

11511185
/// <inheritdoc />
1152-
public Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take)
1186+
public async Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take)
11531187
{
11541188
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
11551189
TItem? parent = Get(key);
11561190

11571191
if (parent?.AllowedContentTypes is null)
11581192
{
1159-
return Task.FromResult(Attempt.FailWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null));
1193+
return Attempt.FailWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null);
1194+
}
1195+
1196+
IEnumerable<ContentTypeSort> allowedContentTypes = parent.AllowedContentTypes;
1197+
foreach (IContentTypeFilter filter in _contentTypeFilters)
1198+
{
1199+
allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key);
11601200
}
11611201

11621202
PagedModel<TItem> result;
1163-
if (parent.AllowedContentTypes.Any() is false)
1203+
if (allowedContentTypes.Any() is false)
11641204
{
11651205
// no content types allowed under parent
11661206
result = new PagedModel<TItem>
@@ -1173,7 +1213,7 @@ public Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
11731213
{
11741214
// Get the sorted keys. Whilst we can't guarantee the order that comes back from GetMany, we can use
11751215
// this to sort the resulting list of allowed children.
1176-
Guid[] sortedKeys = parent.AllowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray();
1216+
Guid[] sortedKeys = allowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray();
11771217

11781218
TItem[] allowedChildren = GetMany(sortedKeys).ToArray();
11791219
result = new PagedModel<TItem>
@@ -1183,7 +1223,7 @@ public Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
11831223
};
11841224
}
11851225

1186-
return Task.FromResult(Attempt.SucceedWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result));
1226+
return Attempt.SucceedWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result);
11871227
}
11881228

11891229
#endregion
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Umbraco.Cms.Core.Composing;
2+
3+
namespace Umbraco.Cms.Core.Services.Filters;
4+
5+
/// <summary>
6+
/// Defines an ordered collection of <see cref="IContentTypeFilter"/>.
7+
/// </summary>
8+
public class ContentTypeFilterCollection : BuilderCollectionBase<IContentTypeFilter>
9+
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="ContentTypeFilterCollection"/> class.
12+
/// </summary>
13+
/// <param name="items">The collection items.</param>
14+
public ContentTypeFilterCollection(Func<IEnumerable<IContentTypeFilter>> items)
15+
: base(items)
16+
{
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Umbraco.Cms.Core.Composing;
2+
3+
namespace Umbraco.Cms.Core.Services.Filters;
4+
5+
/// <summary>
6+
/// Builds an ordered collection of <see cref="IContentTypeFilter"/>.
7+
/// </summary>
8+
public class ContentTypeFilterCollectionBuilder : OrderedCollectionBuilderBase<ContentTypeFilterCollectionBuilder, ContentTypeFilterCollection, IContentTypeFilter>
9+
{
10+
/// <inheritdoc/>
11+
protected override ContentTypeFilterCollectionBuilder This => this;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Umbraco.Cms.Core.Models;
2+
3+
namespace Umbraco.Cms.Core.Services.Filters;
4+
5+
/// <summary>
6+
/// Defines methods for filtering content types after retrieval from the database.
7+
/// </summary>
8+
public interface IContentTypeFilter
9+
{
10+
/// <summary>
11+
/// Filters the content types retrieved for being allowed at the root.
12+
/// </summary>
13+
/// <param name="contentTypes">Retrieved collection of content types.</param>
14+
/// <returns>Filtered collection of content types.</returns>
15+
Task<IEnumerable<TItem>> FilterAllowedAtRootAsync<TItem>(IEnumerable<TItem> contentTypes)
16+
where TItem : IContentTypeComposition;
17+
18+
/// <summary>
19+
/// Filters the content types retrieved for being allowed as children of a parent content type.
20+
/// </summary>
21+
/// <param name="contentTypes">Retrieved collection of content types.</param>
22+
/// <param name="parentKey">The parent content type key.</param>
23+
/// <returns>Filtered collection of content types.</returns>
24+
Task<IEnumerable<ContentTypeSort>> FilterAllowedChildrenAsync(IEnumerable<ContentTypeSort> contentTypes, Guid parentKey);
25+
}

src/Umbraco.Core/Services/MediaTypeService.cs

+31-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Umbraco.Cms.Core.Persistence.Repositories;
88
using Umbraco.Cms.Core.Scoping;
99
using Umbraco.Cms.Core.Services.Changes;
10+
using Umbraco.Cms.Core.Services.Filters;
1011
using Umbraco.Cms.Core.Services.Locking;
1112
using Umbraco.Extensions;
1213

@@ -24,7 +25,8 @@ public MediaTypeService(
2425
IMediaTypeContainerRepository entityContainerRepository,
2526
IEntityRepository entityRepository,
2627
IEventAggregator eventAggregator,
27-
IUserIdKeyResolver userIdKeyResolver)
28+
IUserIdKeyResolver userIdKeyResolver,
29+
ContentTypeFilterCollection contentTypeFilters)
2830
: base(
2931
provider,
3032
loggerFactory,
@@ -34,7 +36,8 @@ public MediaTypeService(
3436
entityContainerRepository,
3537
entityRepository,
3638
eventAggregator,
37-
userIdKeyResolver) => MediaService = mediaService;
39+
userIdKeyResolver,
40+
contentTypeFilters) => MediaService = mediaService;
3841

3942
[Obsolete("Use the constructor with all dependencies instead")]
4043
public MediaTypeService(
@@ -61,6 +64,32 @@ public MediaTypeService(
6164
{
6265
}
6366

67+
[Obsolete("Use the constructor with all dependencies instead")]
68+
public MediaTypeService(
69+
ICoreScopeProvider provider,
70+
ILoggerFactory loggerFactory,
71+
IEventMessagesFactory eventMessagesFactory,
72+
IMediaService mediaService,
73+
IMediaTypeRepository mediaTypeRepository,
74+
IAuditRepository auditRepository,
75+
IMediaTypeContainerRepository entityContainerRepository,
76+
IEntityRepository entityRepository,
77+
IEventAggregator eventAggregator,
78+
IUserIdKeyResolver userIdKeyResolver)
79+
: this(
80+
provider,
81+
loggerFactory,
82+
eventMessagesFactory,
83+
mediaService,
84+
mediaTypeRepository,
85+
auditRepository,
86+
entityContainerRepository,
87+
entityRepository,
88+
eventAggregator,
89+
userIdKeyResolver,
90+
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
91+
{
92+
}
6493

6594
protected override int[] ReadLockIds => MediaTypeLocks.ReadLockIds;
6695

0 commit comments

Comments
 (0)