From a94588497c521a89aa4a78eba44e305d06d91aa8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 28 Jul 2026 23:13:10 +0200 Subject: Fix Folder access filtering --- .../Item/BaseItemRepository.QueryBuilding.cs | 42 ++++++++++++++++++---- .../Item/BaseItemRepository.Querying.cs | 39 +++++++++++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs index a4de9feb05..9d16f7976a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs @@ -395,6 +395,17 @@ public sealed partial class BaseItemRepository return ApplyAccessFiltering(context, baseQuery, filter); } + /// + /// Checks whether the user restricts access to items by parental rating or tags. + /// + /// The query filter. + /// true if the query carries parental restrictions. + private static bool RequiresParentalRestrictions(InternalItemsQuery filter) + => filter.IncludeInheritedTags.Length > 0 + || filter.ExcludeInheritedTags.Length > 0 + || filter.MaxParentalRating is not null + || filter.BlockUnratedItems.Length > 0; + /// /// Applies user access filtering to a query. /// Includes TopParentIds, parental rating, and tag filtering. @@ -412,6 +423,30 @@ public sealed partial class BaseItemRepository baseQuery = baseQuery.Where(e => topParentIds.Contains(e.TopParentId!.Value)); } + baseQuery = ApplyParentalRestrictions(context, baseQuery, filter); + + // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items. + // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those. + if (!filter.IncludeOwnedItems) + { + baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null)); + } + + return baseQuery; + } + + /// + /// Applies the user's parental rating and tag restrictions to a query. + /// + /// The database context. + /// The query to filter. + /// The query filter. + /// The filtered query. + private IQueryable ApplyParentalRestrictions( + JellyfinDbContext context, + IQueryable baseQuery, + InternalItemsQuery filter) + { // Apply parental rating filtering if (filter.MaxParentalRating is not null) { @@ -462,13 +497,6 @@ public sealed partial class BaseItemRepository || e.Type == personTypeName); } - // Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items. - // Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those. - if (!filter.IncludeOwnedItems) - { - baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null)); - } - return baseQuery; } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs index 1ff8d8f863..5ff6e6da49 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs @@ -167,7 +167,10 @@ public sealed partial class BaseItemRepository .Where(album => albumIdsWithMatchingTrack.Contains(album.Id)); } - var orderedAlbums = topAlbumsQuery + // The album is what gets returned, and neither branch above reads it through the + // user's filters, so its own parental restrictions have to be applied here: a + // matching track does not make an album the user may not see visible. + var orderedAlbums = ApplyParentalRestrictions(context, topAlbumsQuery, filter) .OrderByDescending(album => album.DateCreated) .ThenByDescending(album => album.Id); @@ -420,6 +423,40 @@ public sealed partial class BaseItemRepository seriesResults.Add((seasonId, seriesId, maxDate, mostRecentEpisodeId)); } + // Step 5b: A container is what gets returned, so it has to pass the user's access + // filters on its own - a matching episode does not make a Season or Series the user + // may not see visible. Containers that don't pass are replaced by their episode. + if (RequiresParentalRestrictions(filter) && entitiesToFetch.Count > 0) + { + var allowedContainerIds = ApplyParentalRestrictions( + context, + context.BaseItems.AsNoTracking().Where(e => entitiesToFetch.Contains(e.Id)), + filter) + .Select(e => e.Id) + .ToHashSet(); + + for (var i = 0; i < seriesResults.Count; i++) + { + var (seasonId, seriesId, maxDate, mostRecentEpisodeId) = seriesResults[i]; + if (seasonId.HasValue && !allowedContainerIds.Contains(seasonId.Value)) + { + seasonId = null; + } + + if (seriesId.HasValue && !allowedContainerIds.Contains(seriesId.Value)) + { + seriesId = null; + } + + if (seasonId is null && seriesId is null) + { + entitiesToFetch.Add(mostRecentEpisodeId); + } + + seriesResults[i] = (seasonId, seriesId, maxDate, mostRecentEpisodeId); + } + } + // Step 6: Fetch the Season/Series entities we decided to return var entities = entitiesToFetch.Count > 0 ? ApplyNavigations( -- cgit v1.2.3