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