diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs index fe284a1e1119..249de02ceeae 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs @@ -14,4 +14,9 @@ public class TreeEntityPath /// Gets or sets the path of the entity. /// public string Path { get; set; } = null!; + + /// + /// Gets or sets the unique key of the entity. + /// + public Guid Key { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index e1106c57f8a6..d9abd77b2ece 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -73,6 +73,7 @@ public interface IContentRepository : IReadWriteQueryRepository /// Gets paged content items. /// /// Here, can be null but cannot. + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] IEnumerable GetPage( IQuery? query, long pageIndex, @@ -81,5 +82,32 @@ IEnumerable GetPage( IQuery? filter, Ordering? ordering); + /// + /// Gets paged content items. + /// + /// The base query for content items. + /// The page index (zero-based). + /// The number of items per page. + /// Output parameter with total record count. + /// + /// Optional array of property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded (only system properties). + /// + /// Optional filter query. + /// The ordering specification. + /// A collection of content items for the specified page. + /// Here, can be null but cannot. +#pragma warning disable CS0618 // Type or member is obsolete + IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + string[]? propertyAliases, + IQuery? filter, + Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); +#pragma warning restore CS0618 // Type or member is obsolete + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index 6ac6470a8575..cc999b5c17fb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -1,11 +1,43 @@ using System.Collections.Immutable; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Persistence.Repositories; public interface IDocumentRepository : IContentRepository, IReadRepository { + /// + /// Gets paged documents. + /// + /// The base query for documents. + /// The page index (zero-based). + /// The number of items per page. + /// Output parameter with total record count. + /// + /// Optional array of property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded (only system properties). + /// + /// Optional filter query. + /// The ordering specification. + /// + /// Whether to load templates. Set to false for performance optimization when templates are not needed + /// (e.g., collection views). Default is true. + /// + /// A collection of documents for the specified page. + /// Here, can be null but cannot. + IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + string[]? propertyAliases, + IQuery? filter, + Ordering? ordering, + bool loadTemplates) + => GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases, filter, ordering); + /// /// Gets publish/unpublish schedule for a content node. /// diff --git a/src/Umbraco.Core/Security/Authorization/ContentPermissionAuthorizer.cs b/src/Umbraco.Core/Security/Authorization/ContentPermissionAuthorizer.cs index 75c189d11ee2..184916d01e13 100644 --- a/src/Umbraco.Core/Security/Authorization/ContentPermissionAuthorizer.cs +++ b/src/Umbraco.Core/Security/Authorization/ContentPermissionAuthorizer.cs @@ -73,4 +73,11 @@ public async Task IsDeniedForCultures(IUser currentUser, ISet cult // If we can't find the content item(s) then we can't determine whether you are denied access. return result is not (ContentAuthorizationStatus.Success or ContentAuthorizationStatus.NotFound); } + + /// + public async Task> FilterAuthorizedAsync( + IUser currentUser, + IEnumerable contentKeys, + ISet permissionsToCheck) => + await _contentPermissionService.FilterAuthorizedAccessAsync(currentUser, contentKeys, permissionsToCheck); } diff --git a/src/Umbraco.Core/Security/Authorization/IContentPermissionAuthorizer.cs b/src/Umbraco.Core/Security/Authorization/IContentPermissionAuthorizer.cs index e24e423f7ca2..9502e162eb63 100644 --- a/src/Umbraco.Core/Security/Authorization/IContentPermissionAuthorizer.cs +++ b/src/Umbraco.Core/Security/Authorization/IContentPermissionAuthorizer.cs @@ -81,4 +81,30 @@ Task IsDeniedAtRecycleBinLevelAsync(IUser currentUser, string permissionTo Task IsDeniedAtRecycleBinLevelAsync(IUser currentUser, ISet permissionsToCheck); Task IsDeniedForCultures(IUser currentUser, ISet culturesToCheck); + + /// + /// Filters the specified content keys to only those the user has access to. + /// + /// The current user. + /// The keys of the content items to filter. + /// The collection of permissions to authorize. + /// Returns the keys of content items the user has access to. + /// + /// The default implementation falls back to calling + /// for each key individually. Override this method for better performance with batch authorization. + /// + // TODO (V18): Remove default implementation. + async Task> FilterAuthorizedAsync(IUser currentUser, IEnumerable contentKeys, ISet permissionsToCheck) + { + var authorizedKeys = new HashSet(); + foreach (Guid key in contentKeys) + { + if (await IsDeniedAsync(currentUser, [key], permissionsToCheck) == false) + { + authorizedKeys.Add(key); + } + } + + return authorizedKeys; + } } diff --git a/src/Umbraco.Core/Security/Authorization/IMediaPermissionAuthorizer.cs b/src/Umbraco.Core/Security/Authorization/IMediaPermissionAuthorizer.cs index ea6e22818be0..95d2726f941b 100644 --- a/src/Umbraco.Core/Security/Authorization/IMediaPermissionAuthorizer.cs +++ b/src/Umbraco.Core/Security/Authorization/IMediaPermissionAuthorizer.cs @@ -38,4 +38,23 @@ Task IsDeniedAsync(IUser currentUser, Guid mediaKey) /// The current user. /// Returns true if authorization is successful, otherwise false. Task IsDeniedAtRecycleBinLevelAsync(IUser currentUser); + + /// + /// Filters the specified media keys to only those the user has access to. + /// + /// The current user. + /// The keys of the media items to filter. + /// Returns the keys of media items the user has access to. + /// + /// The default implementation falls back to calling + /// for each key individually. Override this method for better performance with batch authorization. + /// + // TODO (V18): Remove default implementation and make this method required. + async Task> FilterAuthorizedAsync(IUser currentUser, IEnumerable mediaKeys) + { + var results = await Task.WhenAll(mediaKeys.Select(async key => + (key, isAuthorized: await IsDeniedAsync(currentUser, [key]) == false))); + + return results.Where(r => r.isAuthorized).Select(r => r.key).ToHashSet(); + } } diff --git a/src/Umbraco.Core/Security/Authorization/MediaPermissionAuthorizer.cs b/src/Umbraco.Core/Security/Authorization/MediaPermissionAuthorizer.cs index af71fcf4af83..1e6ecd45cfbd 100644 --- a/src/Umbraco.Core/Security/Authorization/MediaPermissionAuthorizer.cs +++ b/src/Umbraco.Core/Security/Authorization/MediaPermissionAuthorizer.cs @@ -44,4 +44,8 @@ public async Task IsDeniedAtRecycleBinLevelAsync(IUser currentUser) // If we can't find the media item(s) then we can't determine whether you are denied access. return result is not (MediaAuthorizationStatus.Success or MediaAuthorizationStatus.NotFound); } + + /// + public async Task> FilterAuthorizedAsync(IUser currentUser, IEnumerable mediaKeys) => + await _mediaPermissionService.FilterAuthorizedAccessAsync(currentUser, mediaKeys); } diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 375821ab5fa4..9978eaff4dc8 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -345,7 +345,7 @@ protected override IContent New(string? name, int parentId, IContentType content protected override OperationResult? Delete(IContent content, int userId) => ContentService.Delete(content, userId); protected override IEnumerable GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total) - => ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total); + => ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total, propertyAliases: null, filter: null, ordering: null); protected override ContentEditingOperationStatus Sort(IEnumerable items, int userId) { diff --git a/src/Umbraco.Core/Services/ContentPermissionService.cs b/src/Umbraco.Core/Services/ContentPermissionService.cs index 71ac02d62c18..f5e0fbe0e77f 100644 --- a/src/Umbraco.Core/Services/ContentPermissionService.cs +++ b/src/Umbraco.Core/Services/ContentPermissionService.cs @@ -1,8 +1,8 @@ -using System.Globalization; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services.AuthorizationStatus; using Umbraco.Extensions; @@ -37,19 +37,32 @@ public Task AuthorizeAccessAsync( IEnumerable contentKeys, ISet permissionsToCheck) { - var contentItems = _contentService.GetByIds(contentKeys).ToArray(); + Guid[] keysArray = contentKeys.ToArray(); - if (contentItems.Length == 0) + if (keysArray.Length == 0) + { + return Task.FromResult(ContentAuthorizationStatus.Success); + } + + // Use GetAllPaths instead of loading full content items - we only need paths for authorization + TreeEntityPath[] entityPaths = _entityService.GetAllPaths(UmbracoObjectTypes.Document, keysArray).ToArray(); + + if (entityPaths.Length == 0) { return Task.FromResult(ContentAuthorizationStatus.NotFound); } - if (contentItems.Any(contentItem => user.HasPathAccess(contentItem, _entityService, _appCaches) == false)) + // Check path access using the paths directly + int[]? startNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); + foreach (TreeEntityPath entityPath in entityPaths) { - return Task.FromResult(ContentAuthorizationStatus.UnauthorizedMissingPathAccess); + if (ContentPermissions.HasPathAccess(entityPath.Path, startNodeIds, Constants.System.RecycleBinContent) == false) + { + return Task.FromResult(ContentAuthorizationStatus.UnauthorizedMissingPathAccess); + } } - return Task.FromResult(HasPermissionAccess(user, contentItems.Select(c => c.Path), permissionsToCheck) + return Task.FromResult(HasPermissionAccess(user, entityPaths.Select(p => p.Path), permissionsToCheck) ? ContentAuthorizationStatus.Success : ContentAuthorizationStatus.UnauthorizedMissingPermissionAccess); } @@ -150,6 +163,50 @@ public async Task AuthorizeCultureAccessAsync(IUser : ContentAuthorizationStatus.UnauthorizedMissingCulture; } + /// + public Task> FilterAuthorizedAccessAsync( + IUser user, + IEnumerable contentKeys, + ISet permissionsToCheck) + { + Guid[] keysArray = [.. contentKeys]; + + if (keysArray.Length == 0) + { + return Task.FromResult>(new HashSet()); + } + + // Retrieve paths in a single database query for all keys. + TreeEntityPath[] entityPaths = [.. _entityService.GetAllPaths(UmbracoObjectTypes.Document, keysArray)]; + + if (entityPaths.Length == 0) + { + return Task.FromResult>(new HashSet()); + } + + var authorizedKeys = new HashSet(); + int[]? startNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); + + foreach (TreeEntityPath entityPath in entityPaths) + { + // Check path access + if (ContentPermissions.HasPathAccess(entityPath.Path, startNodeIds, Constants.System.RecycleBinContent) == false) + { + continue; + } + + // Check permission access + EntityPermissionSet permissionSet = _userService.GetPermissionsForPath(user, entityPath.Path); + ISet permissionSetPermissions = permissionSet.GetAllPermissions(); + if (permissionsToCheck.All(p => permissionSetPermissions.Contains(p))) + { + authorizedKeys.Add(entityPath.Key); + } + } + + return Task.FromResult>(authorizedKeys); + } + /// /// Check the implicit/inherited permissions of a user for given content items. /// diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index accdc84712de..aa6c04f48391 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -673,6 +673,7 @@ public IEnumerable GetPagedOfType( pageIndex, pageSize, out totalRecords, + null, filter, ordering); } @@ -705,6 +706,7 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageInde pageIndex, pageSize, out totalRecords, + null, filter, ordering); } @@ -840,7 +842,12 @@ public IEnumerable GetPublishedChildren(int id) } /// + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null) + => GetPagedChildren(id, pageIndex, pageSize, out totalChildren, propertyAliases: null, filter: filter, ordering: ordering); + + /// + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string[]? propertyAliases, IQuery? filter, Ordering? ordering, bool loadTemplates = true) { if (pageIndex < 0) { @@ -859,7 +866,7 @@ public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSi scope.ReadLock(Constants.Locks.ContentTree); IQuery? query = Query()?.Where(x => x.ParentId == id); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, propertyAliases, filter, ordering, loadTemplates); } } @@ -918,7 +925,7 @@ private IEnumerable GetPagedLocked(IQuery? query, long pageI throw new ArgumentNullException(nameof(ordering)); } - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, propertyAliases: null, filter, ordering); } /// @@ -1009,7 +1016,7 @@ public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pag scope.ReadLock(Constants.Locks.ContentTree); IQuery? query = Query()? .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter, ordering); } } diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 013591e67830..ebb7073d64f3 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -88,7 +88,7 @@ public XElement Serialize( while (page * pageSize < total) { IEnumerable children = - _contentService.GetPagedChildren(content.Id, page++, pageSize, out total); + _contentService.GetPagedChildren(content.Id, page++, pageSize, out total, propertyAliases: null, filter: null, ordering: null); SerializeChildren(children, xml, published); } } @@ -678,7 +678,7 @@ private void SerializeChildren(IEnumerable children, XElement xml, boo while (page * pageSize < total) { IEnumerable grandChildren = - _contentService.GetPagedChildren(child.Id, page++, pageSize, out total); + _contentService.GetPagedChildren(child.Id, page++, pageSize, out total, propertyAliases: null, filter: null, ordering: null); // recurse SerializeChildren(grandChildren, childXml, published); diff --git a/src/Umbraco.Core/Services/IContentPermissionService.cs b/src/Umbraco.Core/Services/IContentPermissionService.cs index b6ee049ec733..7982a74993d3 100644 --- a/src/Umbraco.Core/Services/IContentPermissionService.cs +++ b/src/Umbraco.Core/Services/IContentPermissionService.cs @@ -88,4 +88,15 @@ Task AuthorizeBinAccessAsync(IUser user, string perm /// The collection of cultures to authorize. /// A task resolving into a . Task AuthorizeCultureAccessAsync(IUser user, ISet culturesToCheck); + + /// + /// Filters content keys to only those the user has access to. + /// + /// to authorize. + /// The identifiers of the content items to filter. + /// The collection of permissions to authorize. + /// A task resolving into the set of authorized content keys. + // TODO (V18): Remove default implementation. + Task> FilterAuthorizedAccessAsync(IUser user, IEnumerable contentKeys, ISet permissionsToCheck) + => Task.FromResult>(new HashSet()); } diff --git a/src/Umbraco.Core/Services/IContentSearchService{TContent}.cs b/src/Umbraco.Core/Services/IContentSearchService{TContent}.cs index ca8b88d4bc1b..42f9da7cc183 100644 --- a/src/Umbraco.Core/Services/IContentSearchService{TContent}.cs +++ b/src/Umbraco.Core/Services/IContentSearchService{TContent}.cs @@ -1,14 +1,59 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; +/// +/// Defines methods for searching and retrieving child content items of a specified parent, with support for filtering, +/// ordering, and paging. +/// +/// The type of content item to search for. Must implement . public interface IContentSearchService where TContent : class, IContentBase { + /// + /// Searches for children of a content item. + /// + /// The search query. + /// The parent content item key. + /// The ordering. + /// The number of items to skip. + /// The number of items to take. + /// A paged model of content items. + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] Task> SearchChildrenAsync( string? query, Guid? parentId, Ordering? ordering, int skip = 0, - int take = 100); + int take = 100) + => SearchChildrenAsync(query, parentId, propertyAliases: null, ordering: ordering, skip: skip, take: take); + + /// + /// Searches for children of a content item with optional property filtering. + /// + /// The search query. + /// The parent content item key. + /// + /// The property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded. + /// + /// The ordering. + /// + /// Whether to load templates. Set to false for performance optimization when templates are not needed + /// (e.g., collection views). Default is true. Only applies to Document content; ignored for Media/Member. + /// + /// The number of items to skip. + /// The number of items to take. + /// A paged model of content items. +#pragma warning disable CS0618 // Type or member is obsolete + Task> SearchChildrenAsync( + string? query, + Guid? parentId, + string[]? propertyAliases, + Ordering? ordering, + bool loadTemplates = true, + int skip = 0, + int take = 100) + => SearchChildrenAsync(query, parentId, ordering, skip, take); +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ecc39368083c..239d6863c65b 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -272,8 +272,31 @@ IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId /// Total number of documents. /// Query filter. /// Ordering infos. + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null); + /// + /// Gets child documents of a parent with optional property filtering. + /// + /// The parent identifier. + /// The page number. + /// The page size. + /// Total number of documents. + /// + /// The property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded. + /// + /// Query filter. + /// Ordering infos. + /// + /// Whether to load templates. Set to false for performance optimization when templates are not needed + /// (e.g., collection views). Default is true. + /// +#pragma warning disable CS0618 // Type or member is obsolete + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, string[]? propertyAliases, IQuery? filter, Ordering? ordering, bool loadTemplates = true) + => GetPagedChildren(id, pageIndex, pageSize, out totalRecords, filter, ordering); +#pragma warning restore CS0618 // Type or member is obsolete + /// /// Gets descendant documents of a given parent. /// diff --git a/src/Umbraco.Core/Services/IMediaPermissionService.cs b/src/Umbraco.Core/Services/IMediaPermissionService.cs index 00790b4c5ddc..ceda0f226447 100644 --- a/src/Umbraco.Core/Services/IMediaPermissionService.cs +++ b/src/Umbraco.Core/Services/IMediaPermissionService.cs @@ -39,4 +39,14 @@ Task AuthorizeAccessAsync(IUser user, Guid mediaKey) /// to authorize. /// A task resolving into a . Task AuthorizeBinAccessAsync(IUser user); + + /// + /// Filters media keys to only those the user has access to. + /// + /// to authorize. + /// The identifiers of the media items to filter. + /// A task resolving into the set of authorized media keys. + // TODO (V18): Remove default implementation. + Task> FilterAuthorizedAccessAsync(IUser user, IEnumerable mediaKeys) + => Task.FromResult>(new HashSet()); } diff --git a/src/Umbraco.Core/Services/MediaPermissionService.cs b/src/Umbraco.Core/Services/MediaPermissionService.cs index 28276c0f56c3..98274333457a 100644 --- a/src/Umbraco.Core/Services/MediaPermissionService.cs +++ b/src/Umbraco.Core/Services/MediaPermissionService.cs @@ -1,6 +1,8 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services.AuthorizationStatus; namespace Umbraco.Cms.Core.Services; @@ -8,16 +10,13 @@ namespace Umbraco.Cms.Core.Services; /// internal sealed class MediaPermissionService : IMediaPermissionService { - private readonly IMediaService _mediaService; private readonly IEntityService _entityService; private readonly AppCaches _appCaches; public MediaPermissionService( - IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { - _mediaService = mediaService; _entityService = entityService; _appCaches = appCaches; } @@ -25,15 +24,26 @@ public MediaPermissionService( /// public Task AuthorizeAccessAsync(IUser user, IEnumerable mediaKeys) { - foreach (Guid mediaKey in mediaKeys) + Guid[] keysArray = mediaKeys.ToArray(); + + if (keysArray.Length == 0) { - IMedia? media = _mediaService.GetById(mediaKey); - if (media is null) - { - return Task.FromResult(MediaAuthorizationStatus.NotFound); - } + return Task.FromResult(MediaAuthorizationStatus.Success); + } + + // Use GetAllPaths instead of loading full media items - we only need paths for authorization + TreeEntityPath[] entityPaths = _entityService.GetAllPaths(UmbracoObjectTypes.Media, keysArray).ToArray(); + + if (entityPaths.Length == 0) + { + return Task.FromResult(MediaAuthorizationStatus.NotFound); + } - if (user.HasPathAccess(media, _entityService, _appCaches) == false) + // Check path access using the paths directly + int[]? startNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + foreach (TreeEntityPath entityPath in entityPaths) + { + if (ContentPermissions.HasPathAccess(entityPath.Path, startNodeIds, Constants.System.RecycleBinMedia) == false) { return Task.FromResult(MediaAuthorizationStatus.UnauthorizedMissingPathAccess); } @@ -53,4 +63,39 @@ public Task AuthorizeBinAccessAsync(IUser user) => Task.FromResult(user.HasMediaBinAccess(_entityService, _appCaches) ? MediaAuthorizationStatus.Success : MediaAuthorizationStatus.UnauthorizedMissingBinAccess); + + /// + public Task> FilterAuthorizedAccessAsync(IUser user, IEnumerable mediaKeys) + { + Guid[] keysArray = mediaKeys.ToArray(); + + if (keysArray.Length == 0) + { + return Task.FromResult>(new HashSet()); + } + + // Retrieve paths in a single database query for all keys. + TreeEntityPath[] entityPaths = _entityService.GetAllPaths(UmbracoObjectTypes.Media, keysArray).ToArray(); + + if (entityPaths.Length == 0) + { + return Task.FromResult>(new HashSet()); + } + + var authorizedKeys = new HashSet(); + int[]? startNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + + foreach (TreeEntityPath entityPath in entityPaths) + { + // Check path access (media doesn't have granular permissions like content) + if (ContentPermissions.HasPathAccess(entityPath.Path, startNodeIds, Constants.System.RecycleBinMedia) == false) + { + continue; + } + + authorizedKeys.Add(entityPath.Key); + } + + return Task.FromResult>(authorizedKeys); + } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index df60ceee6ebc..651e49ee4028 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -477,7 +477,7 @@ public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(Constants.Locks.MediaTree); - return _mediaRepository.GetPage(Query()?.Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering); + return _mediaRepository.GetPage(Query()?.Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, null, filter, ordering); } /// @@ -506,7 +506,7 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, scope.ReadLock(Constants.Locks.MediaTree); return _mediaRepository.GetPage( - Query()?.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering); + Query()?.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, null, filter, ordering); } /// @@ -609,7 +609,7 @@ public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize scope.ReadLock(Constants.Locks.MediaTree); IQuery? query = Query()?.Where(x => x.ParentId == id); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, propertyAliases: null, filter, ordering); } /// @@ -667,7 +667,7 @@ private IEnumerable GetPagedLocked(IQuery? query, long pageIndex throw new ArgumentNullException(nameof(ordering)); } - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, propertyAliases: null, filter, ordering); } /// @@ -721,7 +721,7 @@ public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSiz scope.ReadLock(Constants.Locks.MediaTree); IQuery? query = Query()?.Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix)); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); + return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter, ordering); } /// diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 1d6fd9a48402..da928f135d73 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -406,7 +406,7 @@ public IEnumerable GetAll(long pageIndex, int pageSize, out long totalR { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(Constants.Locks.MemberTree); - return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName")); + return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: null, Ordering.By("LoginName")); } public IEnumerable GetAll( @@ -435,7 +435,7 @@ public IEnumerable GetAll( int.TryParse(filter, out int filterAsIntId);//considering id,key & name as filter param Guid.TryParse(filter, out Guid filterAsGuid); IQuery? query2 = filter == null ? null : Query()?.Where(x => (x.Name != null && x.Name.Contains(filter)) || x.Username.Contains(filter) || x.Email.Contains(filter) || x.Id == filterAsIntId || x.Key == filterAsGuid ); - return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); + return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, propertyAliases: null, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); } /// @@ -589,7 +589,7 @@ public IEnumerable FindMembersByDisplayName(string displayNameToMatch, throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback } - return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name")); + return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: null, Ordering.By("Name")); } /// @@ -628,7 +628,7 @@ public IEnumerable FindByEmail(string emailStringToMatch, long pageInde throw new ArgumentOutOfRangeException(nameof(matchType)); } - return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email")); + return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: null, Ordering.By("Email")); } /// @@ -667,7 +667,7 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag throw new ArgumentOutOfRangeException(nameof(matchType)); } - return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName")); + return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: null, Ordering.By("LoginName")); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 28778c401af7..f98dac5e9313 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -593,8 +593,37 @@ private string ApplyCustomOrdering(ref Sql sql, Ordering ordering) // would ensure that items without a value always come last, both in ASC and DESC-ending sorts } + /// + /// Gets a page of content items. + /// + /// The query to filter by parent. + /// The page index. + /// The page size. + /// Total number of records. + /// Additional query filter. + /// Ordering information. + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] public abstract IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering); + /// + /// Gets a page of content items with optional property filtering. + /// + /// The query to filter by parent. + /// The page index. + /// The page size. + /// Total number of records. + /// + /// The property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded. + /// + /// Additional query filter. + /// Ordering information. + // TODO (V19): Make this method abstract. +#pragma warning disable CS0618 // Type or member is obsolete + public virtual IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, string[]? propertyAliases, IQuery? filter, Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); +#pragma warning restore CS0618 // Type or member is obsolete + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { var report = new Dictionary(); @@ -756,8 +785,23 @@ protected IEnumerable GetPage( return mapDtos(pagedResult.Items); } + /// + /// Gets property collections for content items, loading all properties. + /// protected IDictionary GetPropertyCollections(List> temps) where T : class, IContentBase + => GetPropertyCollections(temps, propertyAliases: null); + + /// + /// Gets property collections for content items with optional property filtering. + /// + /// The temporary content items. + /// + /// The property aliases to load. If null, all properties are loaded. + /// If empty array, no custom properties are loaded. + /// + protected IDictionary GetPropertyCollections(List> temps, string[]? propertyAliases) + where T : class, IContentBase { var versions = new List(); foreach (TempContent temp in temps) @@ -774,6 +818,14 @@ protected IDictionary GetPropertyCollections(List(); } + // If propertyAliases is an empty array, return empty property collections (no custom properties to load). + if (propertyAliases is { Length: 0 }) + { + return temps.ToDictionary( + temp => temp.VersionId, + _ => new PropertyCollection(new List())); + } + // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId // which then performs a Clustered Index Scan on PK_umbracoPropertyData which means it iterates the entire table which can be enormous! @@ -781,16 +833,35 @@ protected IDictionary GetPropertyCollections(List(versions, Constants.Sql.MaxParameterCount, batch => - SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.VersionId, batch)) - .ToList(); + List propertyDataDtos; + + if (propertyAliases is { Length: > 0 }) + { + // Only specific properties are requested. + // Filter by property alias at SQL level using INNER JOIN to PropertyTypeDto. + propertyDataDtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => + SqlContext.Sql() + .Select() + .From() + .InnerJoin().On((pd, pt) => pd.PropertyTypeId == pt.Id) + .WhereIn(x => x.VersionId, batch) + .WhereIn(x => x.Alias, propertyAliases)) + .ToList(); + } + else + { + // Get all properties (no filtering by property alias). + // This provides the existing behavior from before property alias filtering was implemented. + propertyDataDtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => + SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)) + .ToList(); + } // get PropertyDataDto distinct PropertyTypeDto - var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); + var allPropertyTypeIds = propertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); IEnumerable allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select(r => r.Select(x => x.DataTypeDto)) @@ -800,7 +871,7 @@ protected IDictionary GetPropertyCollections(List x.Id, x => x); - foreach (PropertyDataDto a in allPropertyDataDtos) + foreach (PropertyDataDto a in propertyDataDtos) { a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; } @@ -810,7 +881,7 @@ protected IDictionary GetPropertyCollections(List GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 2b826b2f1c21..55d92f0ebd79 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -247,7 +247,7 @@ protected override string ApplySystemOrdering(ref Sql sql, Ordering private IEnumerable MapDtosToContent( List dtos, bool withCache = false, - bool loadProperties = true, + string[]? propertyAliases = null, bool loadTemplates = true, bool loadVariants = true) { @@ -327,11 +327,15 @@ private IEnumerable MapDtosToContent( .ToDictionary(x => x.Id, x => x); } + // An empty array of propertyAliases indicates that no properties need to be loaded (null = load all properties). + var loadProperties = propertyAliases is { Length: 0 } is false; + IDictionary? properties = null; if (loadProperties) { - // load all properties for all documents from database in 1 query - indexed by version id - properties = GetPropertyCollections(temps); + // load properties for all documents from database in 1 query - indexed by version id + // if propertyAliases is provided, only load those specific properties + properties = GetPropertyCollections(temps, propertyAliases); } // assign templates and properties @@ -364,6 +368,11 @@ private IEnumerable MapDtosToContent( throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); } } + else + { + // When loadProperties is false (propertyAliases is empty array), clear the property collection + temp.Content!.Properties = new PropertyCollection(); + } } if (loadVariants) @@ -890,8 +899,9 @@ public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, i Database.Page(pageIndex + 1, take, sql).Items, true, // load bare minimum, need variants though since this is used to rollback with variants - false, - false); + propertyAliases: [], + loadTemplates: false, + loadVariants: true); } public override IContent? GetVersion(int versionId) @@ -1550,14 +1560,38 @@ public EntityPermissionCollection GetPermissionsForEntity(int entityId) => /// public void AddOrUpdatePermissions(ContentPermissionSet permission) => PermissionRepository.Save(permission); + /// + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] + public override IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, + Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: filter, ordering: ordering, loadTemplates: true); + /// public override IEnumerable GetPage( IQuery? query, long pageIndex, int pageSize, out long totalRecords, + string[]? propertyAliases, IQuery? filter, Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases, filter, ordering, loadTemplates: true); + + /// + public IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + string[]? propertyAliases, + IQuery? filter, + Ordering? ordering, + bool loadTemplates) { Sql? filterSql = null; @@ -1590,7 +1624,7 @@ public override IEnumerable GetPage( pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), + x => MapDtosToContent(x, propertyAliases: propertyAliases, loadTemplates: loadTemplates), filterSql, ordering); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 3caaea2ab88e..a58d725e0628 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -459,7 +459,10 @@ public IEnumerable GetAllPaths(Guid objectType, params Guid[] ke private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) { // NodeId is named Id on TreeEntityPath = use an alias - Sql sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path) + Sql sql = Sql().Select( + x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), + x => x.Path, + x => Alias(x.UniqueId, nameof(TreeEntityPath.Key))) .From().Where(x => x.NodeObjectType == objectType); filter?.Invoke(sql); return Database.Fetch(sql); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 666f34446886..828edcd2e8cd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -61,9 +61,13 @@ protected EntityRepositoryBase( { } +// TODO (V18): Make these fields into read-only properties. + +#pragma warning disable IDE1006 // Naming Styles protected readonly IRepositoryCacheVersionService RepositoryCacheVersionService; protected readonly ICacheSyncService CacheSyncService; +#pragma warning restore IDE1006 // Naming Styles /// /// Gets the logger diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index c6bb7f1a7566..46cfcff0ac5d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -122,6 +122,7 @@ public MediaRepository( protected override MediaRepository This => this; /// + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] public override IEnumerable GetPage( IQuery? query, long pageIndex, @@ -129,7 +130,19 @@ public override IEnumerable GetPage( out long totalRecords, IQuery? filter, Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: filter, ordering: ordering); + + /// + public override IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + string[]? propertyAliases, + IQuery? filter, + Ordering? ordering) { + Sql? filterSql = null; if (filter != null) @@ -146,12 +159,12 @@ public override IEnumerable GetPage( pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), + x => MapDtosToContent(x, propertyAliases: propertyAliases), filterSql, ordering); } - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + private IEnumerable MapDtosToContent(List dtos, bool withCache = false, string[]? propertyAliases = null) { var temps = new List>(); var contentTypes = new Dictionary(); @@ -191,7 +204,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac } // load all properties for all documents from database in 1 query - indexed by version id - IDictionary properties = GetPropertyCollections(temps); + IDictionary properties = GetPropertyCollections(temps, propertyAliases); // assign properties foreach (TempContent temp in temps) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 58f1ae4bc6d3..3ea46925011c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -357,6 +357,7 @@ private void ApplyOrdering(ref Sql sql, Ordering ordering) /// /// Gets paged member results. /// + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] public override IEnumerable GetPage( IQuery? query, long pageIndex, @@ -364,7 +365,21 @@ public override IEnumerable GetPage( out long totalRecords, IQuery? filter, Ordering? ordering) + => GetPage(query, pageIndex, pageSize, out totalRecords, propertyAliases: null, filter: filter, ordering: ordering); + + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + string[]? propertyAliases, + IQuery? filter, + Ordering? ordering) { + Sql? filterSql = null; if (filter != null) @@ -381,7 +396,7 @@ public override IEnumerable GetPage( pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), + x => MapDtosToContent(x, propertyAliases: propertyAliases), filterSql, ordering); } @@ -472,7 +487,7 @@ protected override string ApplySystemOrdering(ref Sql sql, Ordering return base.ApplySystemOrdering(ref sql, ordering); } - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + private IEnumerable MapDtosToContent(List dtos, bool withCache = false, string[]? propertyAliases = null) { var temps = new List>(); var contentTypes = new Dictionary(); @@ -512,7 +527,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac } // load all properties for all documents from database in 1 query - indexed by version id - IDictionary properties = GetPropertyCollections(temps); + IDictionary properties = GetPropertyCollections(temps, propertyAliases); // assign properties foreach (TempContent temp in temps) diff --git a/src/Umbraco.Infrastructure/Services/ContentListViewServiceBase.cs b/src/Umbraco.Infrastructure/Services/ContentListViewServiceBase.cs index 38459d3b9111..2f5a9760ec50 100644 --- a/src/Umbraco.Infrastructure/Services/ContentListViewServiceBase.cs +++ b/src/Umbraco.Infrastructure/Services/ContentListViewServiceBase.cs @@ -26,8 +26,27 @@ protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDa protected abstract Guid DefaultListViewKey { get; } + /// + /// Asynchronously determines whether the specified user has access to the list view item identified by the given + /// key. + /// + /// The user for whom to check access permissions. + /// The unique identifier of the list view item to check access for. + /// A task that represents the asynchronous operation. The task result contains if the user + /// has access to the specified list view item; otherwise, . + [Obsolete("This is no longer used as we now authorize collection view items as a collection via FilterAuthorizedKeysAsync rather than one by one. Scheduled for removal in Umbraco 19.")] protected abstract Task HasAccessToListViewItemAsync(IUser user, Guid key); + /// + /// Filters the specified content keys to only those the user has access to. + /// + /// The user to check access for. + /// The keys of the content items to filter. + /// A set of keys that the user has access to. + // TODO (V18): Make this abstract rather than virtual (it's abstract only to avoid a breaking change). + protected virtual Task> FilterAuthorizedKeysAsync(IUser user, IEnumerable keys) + => Task.FromResult>(new HashSet()); + protected async Task?, ContentCollectionOperationStatus>> GetListViewResultAsync( IUser user, TContent? content, @@ -53,7 +72,11 @@ protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDa return Attempt.FailWithStatus?, ContentCollectionOperationStatus>(orderingAttempt.Status, null); } - PagedModel items = await GetAllowedListViewItemsAsync(user, content?.Key, filter, orderingAttempt.Result, skip, take); + // Extract non-system property aliases from configuration to optimize property loading (we'll optimize and only + // load the properties we need to populate the collection view). + string[]? customPropertyAliases = ExtractCustomPropertyAliases(configurationAttempt.Result, orderingAttempt.Result); + + PagedModel items = await GetAllowedListViewItemsAsync(user, content?.Key, filter, orderingAttempt.Result, customPropertyAliases, skip, take); var result = new ListViewPagedModel { @@ -70,18 +93,17 @@ protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDa string? orderCulture, Direction orderDirection) { - var listViewProperties = listViewConfiguration?.IncludeProperties; + ListViewConfiguration.Property[]? listViewProperties = listViewConfiguration?.IncludeProperties; if (listViewProperties == null || listViewProperties.Length == 0) { return Attempt.FailWithStatus(ContentCollectionOperationStatus.MissingPropertiesInCollectionConfiguration, null); } - var listViewPropertyAliases = listViewProperties + IEnumerable listViewPropertyAliases = listViewProperties .Select(p => p.Alias) .WhereNotNull(); - if (listViewPropertyAliases.Contains(orderBy) == false && orderBy.InvariantEquals("name") == false) { return Attempt.FailWithStatus(ContentCollectionOperationStatus.OrderByNotPartOfCollectionConfiguration, null); @@ -207,9 +229,43 @@ protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDa return await _dataTypeService.GetAsync(configuredListViewKey); } - private async Task> GetAllowedListViewItemsAsync(IUser user, Guid? contentId, string? filter, Ordering? ordering, int skip, int take) + /// + /// Extracts non-system property aliases from the list view configuration. + /// + /// The list view configuration. + /// The ordering information (to ensure the order-by field is included if it's a custom property). + /// + /// An array of custom property aliases to load. Returns empty array if only system properties are configured. + /// + private static string[]? ExtractCustomPropertyAliases(ListViewConfiguration? configuration, Ordering? ordering) + { + if (configuration?.IncludeProperties is null) + { + return null; + } + + // Extract non-system property aliases. + var customAliases = configuration.IncludeProperties + .Where(p => p.IsSystem is false && p.Alias is not null) + .Select(p => p.Alias!) + .ToList(); + + // If ordering by a custom field, ensure it's included in the aliases + // (in case it's not in the configured display columns but is used for sorting). + if (ordering?.IsCustomField is true && + string.IsNullOrEmpty(ordering.OrderBy) is false && + customAliases.Contains(ordering.OrderBy, StringComparer.OrdinalIgnoreCase) is false) + { + customAliases.Add(ordering.OrderBy); + } + + return [.. customAliases]; + } + + private async Task> GetAllowedListViewItemsAsync(IUser user, Guid? contentId, string? filter, Ordering? ordering, string[]? propertyAliases, int skip, int take) { - PagedModel pagedChildren = await _contentSearchService.SearchChildrenAsync(filter, contentId, ordering, skip, take); + // Collection views don't need templates loaded, so we pass loadTemplates: false for performance + PagedModel pagedChildren = await _contentSearchService.SearchChildrenAsync(filter, contentId, propertyAliases, ordering, loadTemplates: false, skip, take); // Filtering out child nodes after getting a paged result is an active choice here, even though the pagination might get off. // This has been the case with this functionality in Umbraco for a long time. @@ -224,21 +280,18 @@ private async Task> GetAllowedListViewItemsAsync(IUser user return pagedResult; } - // TODO: Optimize the way we filter out only the nodes the user is allowed to see - instead of checking one by one private async Task> FilterItemsBasedOnAccessAsync(IUser user, IEnumerable items) { - var filteredItems = new List(); - - foreach (TContent item in items) + TContent[] itemsArray = items.ToArray(); + if (itemsArray.Length == 0) { - var hasAccess = await HasAccessToListViewItemAsync(user, item.Key); - - if (hasAccess) - { - filteredItems.Add(item); - } + return itemsArray; } - return filteredItems; + // Authorize all items at once (so we execute a single database query instead of N queries). + ISet authorizedKeys = await FilterAuthorizedKeysAsync(user, itemsArray.Select(i => i.Key)); + + // Filter items based on authorized keys. + return itemsArray.Where(item => authorizedKeys.Contains(item.Key)); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs index 4bab473d5569..b5217f9143df 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs @@ -2,11 +2,9 @@ using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Infrastructure.Services.Implement; @@ -52,6 +50,7 @@ public ContentListViewService( // We can use an authorizer here, as it already handles all the necessary checks for this filtering. // However, we cannot pass in all the items; we want only the ones that comply, as opposed to // a general response whether the user has access to all nodes. + [Obsolete("This is no longer used as we now authorize collection view items as a collection via FilterAuthorizedKeysAsync rather than one by one. Scheduled for removal in Umbraco 19.")] protected override async Task HasAccessToListViewItemAsync(IUser user, Guid key) { var isDenied = await _contentPermissionAuthorizer.IsDeniedAsync( @@ -61,4 +60,11 @@ protected override async Task HasAccessToListViewItemAsync(IUser user, Gui return isDenied is false; } + + /// + protected override async Task> FilterAuthorizedKeysAsync(IUser user, IEnumerable keys) => + await _contentPermissionAuthorizer.FilterAuthorizedAsync( + user, + keys, + new HashSet { ActionBrowse.ActionLetter }); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentSearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentSearchService.cs index 0aea3ff87179..dd2c83c18418 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentSearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentSearchService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; @@ -16,6 +16,7 @@ public ContentSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger< protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Document; + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] protected override Task> SearchChildrenAsync( IQuery? query, int parentId, @@ -23,11 +24,24 @@ protected override Task> SearchChildrenAsync( long pageNumber, int pageSize, out long total) + => SearchChildrenAsync(query, parentId, propertyAliases: null, ordering: ordering, loadTemplates: true, pageNumber: pageNumber, pageSize: pageSize, total: out total); + + protected override Task> SearchChildrenAsync( + IQuery? query, + int parentId, + string[]? propertyAliases, + Ordering? ordering, + bool loadTemplates, + long pageNumber, + int pageSize, + out long total) => Task.FromResult(_contentService.GetPagedChildren( parentId, pageNumber, pageSize, out total, + propertyAliases, query, - ordering)); + ordering, + loadTemplates)); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentSearchServiceBase.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentSearchServiceBase.cs index bf95880b8bcf..dc5ff729462a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentSearchServiceBase.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentSearchServiceBase.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -24,6 +24,7 @@ protected ContentSearchServiceBase(ISqlContext sqlContext, IIdKeyMap idKeyMap, I protected abstract UmbracoObjectTypes ObjectType { get; } + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] protected abstract Task> SearchChildrenAsync( IQuery? query, int parentId, @@ -32,10 +33,22 @@ protected abstract Task> SearchChildrenAsync( int pageSize, out long total); + protected abstract Task> SearchChildrenAsync( + IQuery? query, + int parentId, + string[]? propertyAliases, + Ordering? ordering, + bool loadTemplates, + long pageNumber, + int pageSize, + out long total); + public async Task> SearchChildrenAsync( string? query, Guid? parentId, + string[]? propertyAliases, Ordering? ordering, + bool loadTemplates = true, int skip = 0, int take = 100) { @@ -55,7 +68,7 @@ public async Task> SearchChildrenAsync( PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); IQuery? contentQuery = ParseQuery(query); - IEnumerable items = await SearchChildrenAsync(contentQuery, parentIdAsInt, ordering, pageNumber, pageSize, out var total); + IEnumerable items = await SearchChildrenAsync(contentQuery, parentIdAsInt, propertyAliases, ordering, loadTemplates, pageNumber, pageSize, out var total); return new PagedModel { Items = items, diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs index 5537697f75b4..a3dbfc621a66 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs @@ -51,6 +51,7 @@ public MediaListViewService( // We can use an authorizer here, as it already handles all the necessary checks for this filtering. // However, we cannot pass in all the items; we want only the ones that comply, as opposed to // a general response whether the user has access to all nodes. + [Obsolete("This is no longer used as we now authorize collection view items as a collection via FilterAuthorizedKeysAsync rather than one by one. Scheduled for removal in Umbraco 19.")] protected override async Task HasAccessToListViewItemAsync(IUser user, Guid key) { var isDenied = await _mediaPermissionAuthorizer.IsDeniedAsync( @@ -59,4 +60,8 @@ protected override async Task HasAccessToListViewItemAsync(IUser user, Gui return isDenied is false; } + + /// + protected override async Task> FilterAuthorizedKeysAsync(IUser user, IEnumerable keys) => + await _mediaPermissionAuthorizer.FilterAuthorizedAsync(user, keys); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaSearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaSearchService.cs index 17bc62e7a967..611680f87d43 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaSearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaSearchService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; @@ -16,6 +16,7 @@ public MediaSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger UmbracoObjectTypes.Media; + [Obsolete("Please use the method overload with all parameters. Scheduled for removal in Umbraco 19.")] protected override Task> SearchChildrenAsync( IQuery? query, int parentId, @@ -23,6 +24,19 @@ protected override Task> SearchChildrenAsync( long pageNumber, int pageSize, out long total) + => SearchChildrenAsync(query, parentId, propertyAliases: null, ordering: ordering, loadTemplates: true, pageNumber: pageNumber, pageSize: pageSize, total: out total); + + protected override Task> SearchChildrenAsync( + IQuery? query, + int parentId, + string[]? propertyAliases, + Ordering? ordering, + bool loadTemplates, + long pageNumber, + int pageSize, + out long total) + + // Note: loadTemplates parameter is ignored for media as media items don't have templates. => Task.FromResult(_mediaService.GetPagedChildren( parentId, pageNumber, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index d5dd9c5be72f..f18db7b7db36 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1413,7 +1413,10 @@ public void Can_Publish_Content_Children() parentId, 0, 500, - out var totalChildren); // we only want the first so page size, etc.. is abitrary + out var totalChildren, + propertyAliases: null, + filter: null, + ordering: null); // we only want the first so page size, etc.. is abitrary // children are published including ... that was released 5 mins ago Assert.IsTrue(children.First(x => x.Id == Subpage.Id).Published); @@ -2256,7 +2259,7 @@ public void Can_Copy_Recursive() Assert.AreEqual(3, ContentService.CountChildren(copy.Id)); var child = ContentService.GetById(Subpage.Id); - var childCopy = ContentService.GetPagedChildren(copy.Id, 0, 500, out var total).First(); + var childCopy = ContentService.GetPagedChildren(copy.Id, 0, 500, out var total, propertyAliases: null, filter: null, ordering: null).First(); Assert.AreEqual(childCopy.Name, child.Name); Assert.AreNotEqual(childCopy.Id, child.Id); Assert.AreNotEqual(childCopy.Key, child.Key); @@ -2815,10 +2818,10 @@ public void Can_Get_Paged_Children() ContentService.Save(c1); } - var entities = ContentService.GetPagedChildren(Constants.System.Root, 0, 6, out var total).ToArray(); + var entities = ContentService.GetPagedChildren(Constants.System.Root, 0, 6, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(6)); Assert.That(total, Is.EqualTo(10)); - entities = ContentService.GetPagedChildren(Constants.System.Root, 1, 6, out total).ToArray(); + entities = ContentService.GetPagedChildren(Constants.System.Root, 1, 6, out total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(4)); Assert.That(total, Is.EqualTo(10)); } @@ -2853,22 +2856,188 @@ public void Can_Get_Paged_Children_Dont_Get_Descendants() } // children in root including the folder - not the descendants in the folder - var entities = ContentService.GetPagedChildren(Constants.System.Root, 0, 6, out var total).ToArray(); + var entities = ContentService.GetPagedChildren(Constants.System.Root, 0, 6, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(6)); Assert.That(total, Is.EqualTo(10)); - entities = ContentService.GetPagedChildren(Constants.System.Root, 1, 6, out total).ToArray(); + entities = ContentService.GetPagedChildren(Constants.System.Root, 1, 6, out total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(4)); Assert.That(total, Is.EqualTo(10)); // children in folder - entities = ContentService.GetPagedChildren(willHaveChildren.Id, 0, 6, out total).ToArray(); + entities = ContentService.GetPagedChildren(willHaveChildren.Id, 0, 6, out total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(6)); Assert.That(total, Is.EqualTo(10)); - entities = ContentService.GetPagedChildren(willHaveChildren.Id, 1, 6, out total).ToArray(); + entities = ContentService.GetPagedChildren(willHaveChildren.Id, 1, 6, out total, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.That(entities.Length, Is.EqualTo(4)); Assert.That(total, Is.EqualTo(10)); } + [Test] + public void GetPagedChildren_With_Null_PropertyAliases_Returns_All_Properties() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - null propertyAliases should load all properties + var retrievedChild = GetSingleChildWithPropertyAliases(parentId, propertyAliases: null); + + // Assert - All properties should have their values loaded + Assert.That(retrievedChild.Properties["title"]?.GetValue(), Is.Not.Null); + Assert.That(retrievedChild.Properties["bodyText"]?.GetValue(), Is.Not.Null); + Assert.That(retrievedChild.Properties["author"]?.GetValue(), Is.Not.Null); + } + + [Test] + public void GetPagedChildren_With_Empty_PropertyAliases_Returns_No_Property_Values() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - empty propertyAliases should load no custom properties + var retrievedChild = GetSingleChildWithPropertyAliases(parentId, propertyAliases: []); + + // Assert - Properties should not be present when propertyAliases is empty + Assert.That(retrievedChild.Properties.Contains("title"), Is.False, "title property should not be present"); + Assert.That(retrievedChild.Properties.Contains("bodyText"), Is.False, "bodyText property should not be present"); + Assert.That(retrievedChild.Properties.Contains("author"), Is.False, "author property should not be present"); + } + + [Test] + public void GetPagedChildren_With_Single_PropertyAlias_Returns_Only_That_Property() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - only "title" should be loaded + var retrievedChild = GetSingleChildWithPropertyAliases(parentId, propertyAliases: ["title"]); + + // Assert - Only "title" property should have its value loaded + Assert.That(retrievedChild.Properties["title"]?.GetValue(), Is.Not.Null); + Assert.That(retrievedChild.Properties["bodyText"]?.GetValue(), Is.Null); + Assert.That(retrievedChild.Properties["author"]?.GetValue(), Is.Null); + } + + [Test] + public void GetPagedChildren_With_Multiple_PropertyAliases_Returns_Only_Those_Properties() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - "title" and "author" should be loaded, but not "bodyText" + var retrievedChild = GetSingleChildWithPropertyAliases(parentId, propertyAliases: ["title", "author"]); + + // Assert - Only "title" and "author" properties should have values loaded + Assert.That(retrievedChild.Properties["title"]?.GetValue(), Is.Not.Null); + Assert.That(retrievedChild.Properties["author"]?.GetValue(), Is.Not.Null); + Assert.That(retrievedChild.Properties["bodyText"]?.GetValue(), Is.Null); + } + + [Test] + public void GetPagedChildren_With_NonExistent_PropertyAlias_Returns_No_Properties() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - non-existent property alias should result in no property values + var retrievedChild = GetSingleChildWithPropertyAliases(parentId, propertyAliases: ["nonExistentProperty"]); + + // Assert - No property values should be loaded since the alias doesn't exist + Assert.That(retrievedChild.Properties["title"]?.GetValue(), Is.Null); + Assert.That(retrievedChild.Properties["bodyText"]?.GetValue(), Is.Null); + Assert.That(retrievedChild.Properties["author"]?.GetValue(), Is.Null); + Assert.That(retrievedChild.Properties.Contains("nonExistentProperty"), Is.False); + } + + [Test] + public void GetPagedChildren_With_LoadTemplates_True_Loads_Template() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - loadTemplates: true (default) should load templates + var retrievedChild = GetSingleChildWithLoadTemplates(parentId, loadTemplates: true); + + // Assert - Template should be loaded + Assert.That(retrievedChild.TemplateId, Is.Not.Null); + } + + [Test] + public void GetPagedChildren_With_LoadTemplates_False_Does_Not_Load_Template() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - loadTemplates: false should not load templates + var retrievedChild = GetSingleChildWithLoadTemplates(parentId, loadTemplates: false); + + // Assert - Template should not be loaded + Assert.That(retrievedChild.TemplateId, Is.Null); + } + + [Test] + public void GetPagedChildren_Default_LoadTemplates_Loads_Template() + { + // Arrange + var parentId = CreateContentWithChildForGetPagedChildrenParameterTests(); + + // Act - default (no loadTemplates specified) should load templates (backwards compatible) + var children = ContentService.GetPagedChildren(parentId, 0, 10, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); + + Assert.That(children.Length, Is.EqualTo(1)); + + // Assert - Template should be loaded by default + Assert.That(children[0].TemplateId, Is.Not.Null); + } + + /// + /// Creates a content type with properties (title, bodyText, author) and a parent with one child. + /// Returns the parent ID for use in GetPagedChildren tests. + /// + private int CreateContentWithChildForGetPagedChildrenParameterTests() + { + var template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + var contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + + var parent = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(parent); + + var child = ContentBuilder.CreateSimpleContent(contentType, "Child", parent.Id); + ContentService.Save(child); + + return parent.Id; + } + + /// + /// Gets the single child of the parent using GetPagedChildren with the specified propertyAliases. + /// Asserts that exactly one child is returned. + /// + private IContent GetSingleChildWithPropertyAliases(int parentId, string[]? propertyAliases) + { + var children = ContentService.GetPagedChildren(parentId, 0, 10, out var total, propertyAliases, filter: null, ordering: null).ToArray(); + + Assert.That(children.Length, Is.EqualTo(1)); + Assert.That(total, Is.EqualTo(1)); + + return children[0]; + } + + /// + /// Gets the single child of the parent using GetPagedChildren with the specified loadTemplates parameter. + /// Asserts that exactly one child is returned. + /// + private IContent GetSingleChildWithLoadTemplates(int parentId, bool loadTemplates) + { + var children = ContentService.GetPagedChildren(parentId, 0, 10, out var total, propertyAliases: null, filter: null, ordering: null, loadTemplates: loadTemplates).ToArray(); + + Assert.That(children.Length, Is.EqualTo(1)); + Assert.That(total, Is.EqualTo(1)); + + return children[0]; + } + [Test] public void PublishingTest() { @@ -3129,7 +3298,7 @@ public async Task Can_Get_Paged_Children_WithFilterAndOrder() } // get all - var list = ContentService.GetPagedChildren(Constants.System.Root, 0, 100, out var total).ToList(); + var list = ContentService.GetPagedChildren(Constants.System.Root, 0, 100, out var total, propertyAliases: null, filter: null, ordering: null).ToList(); Console.WriteLine("ALL"); WriteList(list); @@ -3146,6 +3315,7 @@ public async Task Can_Get_Paged_Children_WithFilterAndOrder() 0, 100, out total, + propertyAliases: null, sqlContext.Query().Where(x => x.Name.Contains("contentX")), Ordering.By("name", culture: langFr.IsoCode)).ToList(); @@ -3158,6 +3328,7 @@ public async Task Can_Get_Paged_Children_WithFilterAndOrder() 0, 100, out total, + propertyAliases: null, sqlContext.Query().Where(x => x.Name.Contains("contentX")), Ordering.By("name", culture: langDa.IsoCode)).ToList(); @@ -3173,6 +3344,7 @@ public async Task Can_Get_Paged_Children_WithFilterAndOrder() 0, 100, out total, + propertyAliases: null, sqlContext.Query().Where(x => x.Name.Contains("contentA")), Ordering.By("name", culture: langFr.IsoCode)).ToList(); @@ -3192,6 +3364,7 @@ public async Task Can_Get_Paged_Children_WithFilterAndOrder() 0, 100, out total, + propertyAliases: null, sqlContext.Query().Where(x => x.Name.Contains("contentA")), Ordering.By("name", Direction.Descending, langFr.IsoCode)).ToList(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index a02b812a7acf..250990bfb0dd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -972,7 +972,7 @@ public void GetPagedResultsByQuery_With_Variant_Names() ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; var query = ScopeProvider.CreateQuery().Where(x => x.ParentId == root.Id); - var result = repository.GetPage(query, 0, 20, out var totalRecords, null, Ordering.By("UpdateDate")); + var result = repository.GetPage(query, 0, 20, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("UpdateDate")); Assert.AreEqual(25, totalRecords); foreach (var r in result) @@ -1015,12 +1015,12 @@ public void GetPagedResultsByQuery_CustomPropertySort() ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; - var result = repository.GetPage(query, 0, 2, out var totalRecords, null, Ordering.By("title", isCustomField: true)); + var result = repository.GetPage(query, 0, 2, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("title", isCustomField: true)); Assert.AreEqual(3, totalRecords); Assert.AreEqual(2, result.Count()); - result = repository.GetPage(query, 1, 2, out totalRecords, null, Ordering.By("title", isCustomField: true)); + result = repository.GetPage(query, 1, 2, out totalRecords, propertyAliases: null, filter: null, Ordering.By("title", isCustomField: true)); Assert.AreEqual(1, result.Count()); } @@ -1047,7 +1047,7 @@ public void GetPagedResultsByQuery_FirstPage() ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlCount = true; - var result = repository.GetPage(query, 0, 1, out var totalRecords, null, Ordering.By("Name")); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("Name")); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); Assert.That(result.Count(), Is.EqualTo(1)); @@ -1070,7 +1070,7 @@ public void GetPagedResultsByQuery_SecondPage() var repository = CreateRepository((IScopeAccessor)provider, out _); var query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 1, 1, out var totalRecords, null, Ordering.By("Name")).ToArray(); + var result = repository.GetPage(query, 1, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("Name")).ToArray(); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); Assert.That(result.Count(), Is.EqualTo(1)); @@ -1087,7 +1087,7 @@ public void GetPagedResultsByQuery_SinglePage() var repository = CreateRepository((IScopeAccessor)provider, out _); var query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 2, out var totalRecords, null, Ordering.By("Name")).ToArray(); + var result = repository.GetPage(query, 0, 2, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("Name")).ToArray(); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); Assert.That(result.Count(), Is.EqualTo(2)); @@ -1104,7 +1104,7 @@ public void GetPagedResultsByQuery_DescendingOrder() var repository = CreateRepository((IScopeAccessor)provider, out _); var query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 1, out var totalRecords, null, Ordering.By("Name", Direction.Descending)).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("Name", Direction.Descending)).ToArray(); Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); Assert.That(result.Count(), Is.EqualTo(1)); @@ -1123,7 +1123,7 @@ public void GetPagedResultsByQuery_FilterMatchingSome() var query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); var filterQuery = ScopeProvider.CreateQuery().Where(x => x.Name.Contains("Page 2")); - var result = repository.GetPage(query, 0, 1, out var totalRecords, filterQuery, Ordering.By("Name")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filterQuery, Ordering.By("Name")).ToArray(); Assert.That(totalRecords, Is.EqualTo(1)); Assert.That(result.Count(), Is.EqualTo(1)); @@ -1142,7 +1142,7 @@ public void GetPagedResultsByQuery_FilterMatchingAll() var query = ScopeProvider.CreateQuery().Where(x => x.Level == 2); var filterQuery = ScopeProvider.CreateQuery().Where(x => x.Name.Contains("text")); - var result = repository.GetPage(query, 0, 1, out var totalRecords, filterQuery, Ordering.By("Name")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filterQuery, Ordering.By("Name")).ToArray(); Assert.That(totalRecords, Is.EqualTo(2)); Assert.That(result.Count(), Is.EqualTo(1)); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index 8db6ba3d6948..cc4b558de983 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -442,7 +442,7 @@ public void GetPagedResultsByQuery_FirstPage() // Act var query = provider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 1, out var totalRecords, null, Ordering.By("SortOrder")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("SortOrder")).ToArray(); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -462,7 +462,7 @@ public void GetPagedResultsByQuery_SecondPage() // Act var query = provider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 1, 1, out var totalRecords, null, Ordering.By("SortOrder")).ToArray(); + var result = repository.GetPage(query, 1, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("SortOrder")).ToArray(); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -482,7 +482,7 @@ public void GetPagedResultsByQuery_SinglePage() // Act var query = provider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 2, out var totalRecords, null, Ordering.By("SortOrder")).ToArray(); + var result = repository.GetPage(query, 0, 2, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("SortOrder")).ToArray(); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -502,7 +502,7 @@ public void GetPagedResultsByQuery_DescendingOrder() // Act var query = provider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 1, out var totalRecords, null, Ordering.By("SortOrder", Direction.Descending)).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("SortOrder", Direction.Descending)).ToArray(); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -522,7 +522,7 @@ public void GetPagedResultsByQuery_AlternateOrder() // Act var query = provider.CreateQuery().Where(x => x.Level == 2); - var result = repository.GetPage(query, 0, 1, out var totalRecords, null, Ordering.By("Name")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter: null, Ordering.By("Name")).ToArray(); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -544,7 +544,7 @@ public void GetPagedResultsByQuery_FilterMatchingSome() var query = provider.CreateQuery().Where(x => x.Level == 2); var filter = provider.CreateQuery().Where(x => x.Name.Contains("File")); - var result = repository.GetPage(query, 0, 1, out var totalRecords, filter, Ordering.By("SortOrder")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter, Ordering.By("SortOrder")).ToArray(); // Assert Assert.That(totalRecords, Is.EqualTo(1)); @@ -566,7 +566,7 @@ public void GetPagedResultsByQuery_FilterMatchingAll() var query = provider.CreateQuery().Where(x => x.Level == 2); var filter = provider.CreateQuery().Where(x => x.Name.Contains("Test")); - var result = repository.GetPage(query, 0, 1, out var totalRecords, filter, Ordering.By("SortOrder")).ToArray(); + var result = repository.GetPage(query, 0, 1, out var totalRecords, propertyAliases: null, filter, Ordering.By("SortOrder")).ToArray(); // Assert Assert.That(totalRecords, Is.EqualTo(2)); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs index a92f64d63752..23d35aa06df9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs @@ -32,7 +32,7 @@ public override void CreateTestData() var rootContent = ContentService.GetRootContent().First(); _rootPage = rootContent; - var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList(); + var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _, propertyAliases: null, filter: null, ordering: null).ToList(); _testPage = subPages[0]; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs index 6695f7eedfdd..c8b17d58369c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs @@ -112,7 +112,7 @@ void VerifyCopy(IContent? copiedRoot) Assert.AreNotEqual(root1.Key, copiedRoot.Key); Assert.AreEqual(root1.Name, copiedRoot.Name); - var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total).ToArray(); + var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); if (includeDescendants) { @@ -179,7 +179,7 @@ void VerifyCopy(IContent? copiedRoot) Assert.IsTrue(copiedRoot.HasIdentity); Assert.AreNotEqual(root.Key, copiedRoot.Key); Assert.AreEqual(root.Name, copiedRoot.Name); - var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total).ToArray(); + var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); if (includeDescendants) { @@ -221,7 +221,7 @@ void VerifyCopy(IContent? copiedRoot) Assert.IsTrue(copiedRoot.HasIdentity); Assert.AreNotEqual(root.Key, copiedRoot.Key); Assert.AreEqual(root.Name, copiedRoot.Name); - var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total).ToArray(); + var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total, propertyAliases: null, filter: null, ordering: null).ToArray(); if (includeDescendants) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Sort.cs index 6699361ae765..76fcb35542dc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Sort.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Sort.cs @@ -15,7 +15,7 @@ public async Task Can_Sort_Children() { var root = await CreateRootContentWithTenChildren(); - var originalChildren = ContentService.GetPagedChildren(root.Id, 0, 100, out _).ToArray(); + var originalChildren = ContentService.GetPagedChildren(root.Id, 0, 100, out _, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.AreEqual(10, originalChildren.Length); var sortingModels = originalChildren.Reverse().Select((child, index) => new SortingModel { Key = child.Key, SortOrder = index }); @@ -24,7 +24,7 @@ public async Task Can_Sort_Children() Assert.AreEqual(ContentEditingOperationStatus.Success, result); var actualChildrenKeys = ContentService - .GetPagedChildren(root.Id, 0, 100, out _) + .GetPagedChildren(root.Id, 0, 100, out _, propertyAliases: null, filter: null, ordering: null) .OrderBy(c => c.SortOrder) .Select(c => c.Key) .ToArray(); @@ -60,7 +60,7 @@ await ContentEditingService.CreateAsync( Constants.Security.SuperUserKey); } - var originalRoots = ContentService.GetPagedChildren(Constants.System.Root, 0, 100, out _).ToArray(); + var originalRoots = ContentService.GetPagedChildren(Constants.System.Root, 0, 100, out _, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.AreEqual(10, originalRoots.Length); var sortingModels = originalRoots.Reverse().Select((root, index) => new SortingModel { Key = root.Key, SortOrder = index }); @@ -73,7 +73,7 @@ await ContentEditingService.CreateAsync( Assert.AreEqual(ContentEditingOperationStatus.Success, result); var actualRootKeys = ContentService - .GetPagedChildren(Constants.System.Root, 0, 100, out _) + .GetPagedChildren(Constants.System.Root, 0, 100, out _, propertyAliases: null, filter: null, ordering: null) .OrderBy(c => c.SortOrder) .Select(c => c.Key) .ToArray(); @@ -90,7 +90,7 @@ public async Task Cannot_Sort_Unknown_Children() { var root = await CreateRootContentWithTenChildren(); - var originalChildren = ContentService.GetPagedChildren(root.Id, 0, 100, out _).ToArray(); + var originalChildren = ContentService.GetPagedChildren(root.Id, 0, 100, out _, propertyAliases: null, filter: null, ordering: null).ToArray(); Assert.AreEqual(10, originalChildren.Length); var sortingModels = new[] @@ -103,7 +103,7 @@ public async Task Cannot_Sort_Unknown_Children() Assert.AreEqual(ContentEditingOperationStatus.SortingInvalid, result); var actualChildrenKeys = ContentService - .GetPagedChildren(root.Id, 0, 100, out _) + .GetPagedChildren(root.Id, 0, 100, out _, propertyAliases: null, filter: null, ordering: null) .OrderBy(c => c.SortOrder) .Select(c => c.Key) .ToArray(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index f993f70009f7..2957e3082232 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -379,7 +379,7 @@ private void WriteEvents() #region Utils private IEnumerable Children(IContent content) - => ContentService.GetPagedChildren(content.Id, 0, int.MaxValue, out _); + => ContentService.GetPagedChildren(content.Id, 0, int.MaxValue, out _, propertyAliases: null, filter: null, ordering: null); #endregion diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs index 77c730ae1834..4aecd56f2403 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs @@ -646,7 +646,7 @@ public async Task Can_Filter_List_View_Items(string filter, string expectedName) // Arrange var root = await CreateRootContentWithFiveChildrenAsListViewItems(); - var allChildren = ContentService.GetPagedChildren(root.Id, 0, 10, out _).ToArray(); + var allChildren = ContentService.GetPagedChildren(root.Id, 0, 10, out _, propertyAliases: null, filter: null, ordering: null).ToArray(); // Act var result = await ContentListViewService.GetListViewItemsByKeyAsync( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs index 962aa43bd9dd..952d32f54233 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs @@ -43,7 +43,7 @@ public override void CreateTestData() Mock.Of(), Mock.Of()); var rootContent = ContentService.GetRootContent().First(); - var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList(); + var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _, propertyAliases: null, filter: null, ordering: null).ToList(); _firstSubPage = subPages[0]; _secondSubPage = subPages[1]; _thirdSubPage = subPages[2];