Published status filtering (#18281)

* Initial refactor (pending more tests)

* Fix structural querying across changing publish states + add tests accordingly

* Add tests to validate ancestor and descendant order

* Remove axis querying from published status filtering

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
Kenn Jacobsen
2025-02-25 13:25:33 +01:00
committed by GitHub
parent c19b2286b3
commit 790c451df1
40 changed files with 2730 additions and 2426 deletions

View File

@@ -16,12 +16,18 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
private readonly IPublishedMediaCache _publishedMediaCache;
private readonly ILogger<ApiMediaQueryService> _logger;
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
private readonly IPublishedMediaStatusFilteringService _publishedMediaStatusFilteringService;
public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger<ApiMediaQueryService> logger, IMediaNavigationQueryService mediaNavigationQueryService)
public ApiMediaQueryService(
IPublishedMediaCache publishedMediaCache,
ILogger<ApiMediaQueryService> logger,
IMediaNavigationQueryService mediaNavigationQueryService,
IPublishedMediaStatusFilteringService publishedMediaStatusFilteringService)
{
_publishedMediaCache = publishedMediaCache;
_logger = logger;
_mediaNavigationQueryService = mediaNavigationQueryService;
_publishedMediaStatusFilteringService = publishedMediaStatusFilteringService;
}
/// <inheritdoc/>
@@ -71,7 +77,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
break;
}
currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService);
currentChildren = resolvedMedia.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService);
}
return resolvedMedia;
@@ -104,7 +110,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
? mediaCache.GetById(parentKey)
: TryGetByPath(childrenOf, mediaCache);
return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty<IPublishedContent>();
return parent?.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService) ?? Array.Empty<IPublishedContent>();
}
private IEnumerable<IPublishedContent>? ApplyFilters(IEnumerable<IPublishedContent> source, IEnumerable<string> filters)

View File

@@ -1,12 +1,13 @@
using System.Diagnostics;
using System.Linq.Expressions;
using System.Runtime.Versioning;
using System.Text;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.Template.Query;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Models.TemplateQuery;
using Umbraco.Cms.Core.PublishedCache;
@@ -20,14 +21,46 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Query;
public class ExecuteTemplateQueryController : TemplateQueryControllerBase
{
private readonly IPublishedContentQuery _publishedContentQuery;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly IContentTypeService _contentTypeService;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
private static readonly string _indent = $"{Environment.NewLine} ";
public ExecuteTemplateQueryController(
IPublishedContentQuery publishedContentQuery,
IPublishedValueFallback publishedValueFallback,
IContentTypeService contentTypeService,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_publishedContentQuery = publishedContentQuery;
_publishedValueFallback = publishedValueFallback;
_contentTypeService = contentTypeService;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ExecuteTemplateQueryController(
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
IPublishedValueFallback publishedValueFallback,
IContentTypeService contentTypeService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
publishedContentQuery,
publishedValueFallback,
contentTypeService,
documentNavigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ExecuteTemplateQueryController(
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
@@ -35,13 +68,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
IContentTypeService contentTypeService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService)
: this(
publishedContentQuery,
publishedValueFallback,
contentTypeService,
documentNavigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
_publishedContentQuery = publishedContentQuery;
_variationContextAccessor = variationContextAccessor;
_publishedValueFallback = publishedValueFallback;
_contentTypeService = contentTypeService;
_contentCache = contentCache;
_documentNavigationQueryService = documentNavigationQueryService;
}
[HttpPost("execute")]
@@ -118,13 +151,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
queryExpression.Append(".ChildrenOfType(\"").Append(model.DocumentTypeAlias).Append("\")");
return rootContent == null
? Enumerable.Empty<IPublishedContent>()
: rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias);
: rootContent.ChildrenOfType(_documentNavigationQueryService, _publishedContentStatusFilteringService, model.DocumentTypeAlias);
}
queryExpression.Append(".Children()");
return rootContent == null
? Enumerable.Empty<IPublishedContent>()
: rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService);
: rootContent.Children(_documentNavigationQueryService, _publishedContentStatusFilteringService);
}
private IEnumerable<IPublishedContent> ApplyFiltering(IEnumerable<TemplateQueryExecuteFilterPresentationModel>? filters, IEnumerable<IPublishedContent> contentQuery, StringBuilder queryExpression)

View File

@@ -42,7 +42,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
requestSettings.OnChange(settings => _requestSettings = settings);
}
[Obsolete("Use constructor that takes an IPublishStatusQueryService instead, scheduled for removal in v17")]
[Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")]
public ApiContentRouteBuilder(
IApiContentPathProvider apiContentPathProvider,
IOptions<GlobalSettings> globalSettings,
@@ -80,7 +80,12 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
contentPath = contentPath.EnsureStartsWith("/");
IPublishedContent root = GetRoot(content, isPreview);
IPublishedContent? root = GetRoot(content, isPreview);
if (root is null)
{
return null;
}
var rootPath = root.UrlSegment(_variationContextAccessor, culture) ?? string.Empty;
if (_globalSettings.HideTopLevelNodeFromPath == false)
@@ -127,19 +132,21 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
private static bool IsInvalidContentPath(string? path) => path.IsNullOrWhiteSpace() || "#".Equals(path);
private IPublishedContent GetRoot(IPublishedContent content, bool isPreview)
private IPublishedContent? GetRoot(IPublishedContent content, bool isPreview)
{
if (isPreview is false)
if (content.Level == 1)
{
return content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService);
return content;
}
_navigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
IEnumerable<IPublishedContent> rootContent = rootKeys.Select(x => _contentCache.GetById(true, x)).WhereNotNull();
if (_navigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable<Guid> ancestorKeys) is false)
{
return null;
}
// in very edge case scenarios during preview, content.Root() does not map to the root.
// we'll code our way around it for the time being.
return rootContent.FirstOrDefault(root => root.IsAncestorOrSelf(content))
?? content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService);
Guid[] ancestorKeysAsArray = ancestorKeys as Guid[] ?? ancestorKeys.ToArray();
return ancestorKeysAsArray.Length > 0
? _contentCache.GetById(isPreview, ancestorKeysAsArray.Last())
: content;
}
}

View File

@@ -387,6 +387,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IPublishStatusManagementService>(x => x.GetRequiredService<PublishStatusService>());
Services.AddUnique<IPublishStatusQueryService>(x => x.GetRequiredService<PublishStatusService>());
Services.AddUnique<IPublishedContentStatusFilteringService, PublishedContentStatusFilteringService>();
Services.AddUnique<IPublishedMediaStatusFilteringService, PublishedMediaStatusFilteringService>();
// Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced
Services.AddUnique<IHtmlSanitizer, NoopHtmlSanitizer>();
Services.AddUnique<IMarkdownSanitizer, NoopMarkdownSanitizer>();

View File

@@ -7,15 +7,14 @@
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@inject IPublishedValueFallback PublishedValueFallback
@inject IPublishedUrlProvider PublishedUrlProvider
@inject IVariationContextAccessor VariationContextAccessor
@inject IPublishedContentCache PublishedContentCache
@inject IDocumentNavigationQueryService DocumentNavigationQueryService
@inject IPublishedContentStatusFilteringService PublishedContentStatusFilteringService
@*
This snippet creates links for every single page (no matter how deep) below
the page currently being viewed by the website visitor, displayed as nested unordered HTML lists.
*@
@{ var selection = Model?.Content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); }
@{ var selection = Model?.Content.Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); }
@* Ensure that the Current Page has children *@
@if (selection?.Length > 0)
@@ -34,7 +33,7 @@
@* if this child page has any children, where the property umbracoNaviHide is not True *@
@{
var children = item
.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService)
.Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService)
.Where(x => x.IsVisible(PublishedValueFallback))
.ToArray();
@@ -68,7 +67,7 @@
@* if the page has any children, where the property umbracoNaviHide is not True *@
@{
var children = item
.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService)
.Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService)
.Where(x => x.IsVisible(PublishedValueFallback))
.ToArray();

View File

@@ -7,9 +7,8 @@
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@inject IPublishedValueFallback PublishedValueFallback
@inject IPublishedUrlProvider PublishedUrlProvider
@inject IVariationContextAccessor VariationContextAccessor
@inject IPublishedContentCache PublishedContentCache
@inject IDocumentNavigationQueryService DocumentNavigationQueryService
@inject IPublishedContentStatusFilteringService PublishedContentStatusFilteringService
@*
This snippet makes a list of links of all visible pages of the site, as nested unordered HTML lists.
@@ -17,7 +16,7 @@
- It uses a local method called Traverse() to select and display the markup and links.
*@
@{ var selection = Model?.Content.Root(PublishedContentCache, DocumentNavigationQueryService); }
@{ var selection = Model?.Content.Root(DocumentNavigationQueryService, PublishedContentStatusFilteringService); }
<div class="sitemap">
@* Render the sitemap by passing the root node to the traverse method, below *@
@@ -33,7 +32,7 @@
@* Select visible children *@
var selection = node
.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService)
.Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService)
.Where(x => x.IsVisible(PublishedValueFallback) && x.Level <= maxLevelForSitemap)
.ToArray();

View File

@@ -1,6 +1,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
@@ -24,11 +26,75 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo
private readonly ILoggerFactory _loggerFactory;
private readonly UriUtility _uriUtility;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IPublishedContentCache _publishedContentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
private readonly IEventMessagesFactory _eventMessagesFactory;
private readonly ContentSettings _contentSettings;
public AddUnroutableContentWarningsWhenPublishingNotificationHandler(
IPublishedRouter publishedRouter,
IUmbracoContextAccessor umbracoContextAccessor,
ILanguageService languageService,
ILocalizedTextService localizedTextService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILoggerFactory loggerFactory,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService,
IEventMessagesFactory eventMessagesFactory,
IOptions<ContentSettings> contentSettings)
{
_publishedRouter = publishedRouter;
_umbracoContextAccessor = umbracoContextAccessor;
_languageService = languageService;
_localizedTextService = localizedTextService;
_contentService = contentService;
_variationContextAccessor = variationContextAccessor;
_loggerFactory = loggerFactory;
_uriUtility = uriUtility;
_publishedUrlProvider = publishedUrlProvider;
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
_eventMessagesFactory = eventMessagesFactory;
_contentSettings = contentSettings.Value;
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public AddUnroutableContentWarningsWhenPublishingNotificationHandler(
IPublishedRouter publishedRouter,
IUmbracoContextAccessor umbracoContextAccessor,
ILanguageService languageService,
ILocalizedTextService localizedTextService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILoggerFactory loggerFactory,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentCache publishedContentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService,
IEventMessagesFactory eventMessagesFactory,
IOptions<ContentSettings> contentSettings)
: this(
publishedRouter,
umbracoContextAccessor,
languageService,
localizedTextService,
contentService,
variationContextAccessor,
loggerFactory,
uriUtility,
publishedUrlProvider,
navigationQueryService,
publishedContentStatusFilteringService,
eventMessagesFactory,
contentSettings)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public AddUnroutableContentWarningsWhenPublishingNotificationHandler(
IPublishedRouter publishedRouter,
IUmbracoContextAccessor umbracoContextAccessor,
@@ -43,20 +109,21 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo
IDocumentNavigationQueryService navigationQueryService,
IEventMessagesFactory eventMessagesFactory,
IOptions<ContentSettings> contentSettings)
: this(
publishedRouter,
umbracoContextAccessor,
languageService,
localizedTextService,
contentService,
variationContextAccessor,
loggerFactory,
uriUtility,
publishedUrlProvider,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>(),
eventMessagesFactory,
contentSettings)
{
_publishedRouter = publishedRouter;
_umbracoContextAccessor = umbracoContextAccessor;
_languageService = languageService;
_localizedTextService = localizedTextService;
_contentService = contentService;
_variationContextAccessor = variationContextAccessor;
_loggerFactory = loggerFactory;
_uriUtility = uriUtility;
_publishedUrlProvider = publishedUrlProvider;
_publishedContentCache = publishedContentCache;
_navigationQueryService = navigationQueryService;
_eventMessagesFactory = eventMessagesFactory;
_contentSettings = contentSettings.Value;
}
public async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken)
@@ -99,8 +166,8 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo
_loggerFactory.CreateLogger<IContent>(),
_uriUtility,
_publishedUrlProvider,
_publishedContentCache,
_navigationQueryService)).ToArray();
_navigationQueryService,
_publishedContentStatusFilteringService)).ToArray();
EventMessages eventMessages = _eventMessagesFactory.Get();

File diff suppressed because it is too large Load Diff

View File

@@ -90,23 +90,23 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
private IEnumerable<IPublishedContent> GetChildren()
{
INavigationQueryService? navigationQueryService;
IPublishedCache? publishedCache;
IPublishedStatusFilteringService? publishedStatusFilteringService;
switch (ContentType.ItemType)
{
case PublishedItemType.Content:
publishedCache = StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>();
navigationQueryService = StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>();
publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>();
break;
case PublishedItemType.Media:
publishedCache = StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaCache>();
navigationQueryService = StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationQueryService>();
publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaStatusFilteringService>();
break;
default:
throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType);
}
return this.Children(_variationContextAccessor, publishedCache, navigationQueryService);
return this.Children(navigationQueryService, publishedStatusFilteringService);
}
}
}

View File

@@ -188,7 +188,7 @@ public class PublishedValueFallback : IPublishedValueFallback
IPublishedProperty? property; // if we are here, content's property has no value
do
{
content = content?.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(), StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>());
content = content?.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(), StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
IPublishedPropertyType? propertyType = content?.ContentType.GetPropertyType(alias);

View File

@@ -70,7 +70,7 @@ public sealed class InternalPublishedContent : IPublishedContent
public PublishedItemType ItemType => PublishedItemType.Content;
[Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public IPublishedContent? Parent => this.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(), StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>());
public IPublishedContent? Parent => this.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(), StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
public bool IsDraft(string? culture = null) => false;
@@ -78,9 +78,8 @@ public sealed class InternalPublishedContent : IPublishedContent
[Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public IEnumerable<IPublishedContent> Children => this.Children(
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>());
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
public IEnumerable<IPublishedContent> ChildrenForAllCultures => Children;
@@ -102,7 +101,7 @@ public sealed class InternalPublishedContent : IPublishedContent
IPublishedContent? content = this;
while (content != null && (property == null || property.HasValue() == false))
{
content = content.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(), StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>());
content = content.Parent<IPublishedContent>(StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(), StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
property = content?.GetProperty(alias);
}

View File

@@ -18,8 +18,8 @@ public class AliasUrlProvider : IUrlProvider
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly ISiteDomainMapper _siteDomainMapper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
private readonly UriUtility _uriUtility;
private RequestHandlerSettings _requestConfig;
@@ -29,21 +29,62 @@ public class AliasUrlProvider : IUrlProvider
UriUtility uriUtility,
IPublishedValueFallback publishedValueFallback,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_requestConfig = requestConfig.CurrentValue;
_siteDomainMapper = siteDomainMapper;
_uriUtility = uriUtility;
_publishedValueFallback = publishedValueFallback;
_umbracoContextAccessor = umbracoContextAccessor;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
requestConfig.OnChange(x => _requestConfig = x);
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public AliasUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestConfig,
ISiteDomainMapper siteDomainMapper,
UriUtility uriUtility,
IPublishedValueFallback publishedValueFallback,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
requestConfig,
siteDomainMapper,
uriUtility,
publishedValueFallback,
umbracoContextAccessor,
navigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public AliasUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestConfig,
ISiteDomainMapper siteDomainMapper,
UriUtility uriUtility,
IPublishedValueFallback publishedValueFallback,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
: this(
requestConfig,
siteDomainMapper,
uriUtility,
publishedValueFallback,
umbracoContextAccessor,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public AliasUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestConfig,
ISiteDomainMapper siteDomainMapper,
@@ -56,8 +97,8 @@ public class AliasUrlProvider : IUrlProvider
uriUtility,
publishedValueFallback,
umbracoContextAccessor,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
@@ -108,7 +149,7 @@ public class AliasUrlProvider : IUrlProvider
while (domainUris == null && n != null)
{
// move to parent node
n = n.Parent<IPublishedContent>(_contentCache, _navigationQueryService);
n = n.Parent<IPublishedContent>(_navigationQueryService, _publishedContentStatusFilteringService);
domainUris = n == null
? null
: DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current, false);

View File

@@ -24,14 +24,46 @@ public class ContentFinderByUrlAlias : IContentFinder
private readonly ILogger<ContentFinderByUrlAlias> _logger;
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
/// <summary>
/// Initializes a new instance of the <see cref="ContentFinderByUrlAlias" /> class.
/// </summary>
public ContentFinderByUrlAlias(
ILogger<ContentFinderByUrlAlias> logger,
IPublishedValueFallback publishedValueFallback,
IUmbracoContextAccessor umbracoContextAccessor,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_publishedValueFallback = publishedValueFallback;
_umbracoContextAccessor = umbracoContextAccessor;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
_logger = logger;
}
[Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")]
public ContentFinderByUrlAlias(
ILogger<ContentFinderByUrlAlias> logger,
IPublishedValueFallback publishedValueFallback,
IVariationContextAccessor variationContextAccessor,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishStatusQueryService publishStatusQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
logger,
publishedValueFallback,
umbracoContextAccessor,
documentNavigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")]
public ContentFinderByUrlAlias(
ILogger<ContentFinderByUrlAlias> logger,
IPublishedValueFallback publishedValueFallback,
@@ -40,17 +72,17 @@ public class ContentFinderByUrlAlias : IContentFinder
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishStatusQueryService publishStatusQueryService)
: this(
logger,
publishedValueFallback,
umbracoContextAccessor,
documentNavigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
_publishedValueFallback = publishedValueFallback;
_variationContextAccessor = variationContextAccessor;
_umbracoContextAccessor = umbracoContextAccessor;
_contentCache = contentCache;
_documentNavigationQueryService = documentNavigationQueryService;
_publishStatusQueryService = publishStatusQueryService;
_logger = logger;
}
[Obsolete("Please use constructor that takes an IPublishStatusQueryService instead. Scheduled removal in v17")]
[Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")]
public ContentFinderByUrlAlias(
ILogger<ContentFinderByUrlAlias> logger,
IPublishedValueFallback publishedValueFallback,
@@ -61,11 +93,9 @@ public class ContentFinderByUrlAlias : IContentFinder
: this(
logger,
publishedValueFallback,
variationContextAccessor,
umbracoContextAccessor,
contentCache,
documentNavigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
@@ -169,14 +199,14 @@ public class ContentFinderByUrlAlias : IContentFinder
if (rootNodeId > 0)
{
IPublishedContent? rootNode = cache?.GetById(rootNodeId);
return rootNode?.Descendants(_variationContextAccessor, _contentCache, _documentNavigationQueryService, _publishStatusQueryService).FirstOrDefault(x => IsMatch(x, test1, test2));
return rootNode?.Descendants(_documentNavigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(x => IsMatch(x, test1, test2));
}
if (cache is not null)
{
foreach (IPublishedContent rootContent in cache.GetAtRoot())
{
IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _documentNavigationQueryService, _publishStatusQueryService)
IPublishedContent? c = rootContent.DescendantsOrSelf(_documentNavigationQueryService, _publishedContentStatusFilteringService)
.FirstOrDefault(x => IsMatch(x, test1, test2));
if (c != null)
{

View File

@@ -23,8 +23,8 @@ public class DefaultUrlProvider : IUrlProvider
private readonly ILogger<DefaultUrlProvider> _logger;
private readonly ISiteDomainMapper _siteDomainMapper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
private readonly UriUtility _uriUtility;
private RequestHandlerSettings _requestSettings;
@@ -35,8 +35,8 @@ public class DefaultUrlProvider : IUrlProvider
IUmbracoContextAccessor umbracoContextAccessor,
UriUtility uriUtility,
ILocalizationService localizationService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_requestSettings = requestSettings.CurrentValue;
_logger = logger;
@@ -44,13 +44,58 @@ public class DefaultUrlProvider : IUrlProvider
_umbracoContextAccessor = umbracoContextAccessor;
_uriUtility = uriUtility;
_localizationService = localizationService;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
requestSettings.OnChange(x => _requestSettings = x);
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public DefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
ISiteDomainMapper siteDomainMapper,
IUmbracoContextAccessor umbracoContextAccessor,
UriUtility uriUtility,
ILocalizationService localizationService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
requestSettings,
logger,
siteDomainMapper,
umbracoContextAccessor,
uriUtility,
localizationService,
navigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public DefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
ISiteDomainMapper siteDomainMapper,
IUmbracoContextAccessor umbracoContextAccessor,
UriUtility uriUtility,
ILocalizationService localizationService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
: this(
requestSettings,
logger,
siteDomainMapper,
umbracoContextAccessor,
uriUtility,
localizationService,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public DefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
@@ -65,8 +110,8 @@ public class DefaultUrlProvider : IUrlProvider
umbracoContextAccessor,
uriUtility,
localizationService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
@@ -101,7 +146,7 @@ public class DefaultUrlProvider : IUrlProvider
// n is null at root
while (domainUris == null && n != null)
{
n = n.Parent<IPublishedContent>(_contentCache, _navigationQueryService); // move to parent node
n = n.Parent<IPublishedContent>(_navigationQueryService, _publishedContentStatusFilteringService); // move to parent node
domainUris = n == null
? null
: DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current);

View File

@@ -17,21 +17,7 @@ namespace Umbraco.Cms.Core.Routing
{
#region Document Culture
/// <summary>
/// Gets the culture assigned to a document by domains, in the context of a current Uri.
/// </summary>
/// <param name="contentId">The document identifier.</param>
/// <param name="contentPath">The document path.</param>
/// <param name="current">An optional current Uri.</param>
/// <param name="umbracoContext">An Umbraco context.</param>
/// <param name="siteDomainMapper">The site domain helper.</param>
/// <returns>The culture assigned to the document by domains.</returns>
/// <remarks>
/// <para>In 1:1 multilingual setup, a document contains several cultures (there is not
/// one document per culture), and domains, withing the context of a current Uri, assign
/// a culture to that document.</para>
/// </remarks>
[Obsolete("Please use the method taking all parameters. This overload will be removed in V17.")]
[Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")]
public static string? GetCultureFromDomains(
int contentId,
string contentPath,
@@ -45,8 +31,28 @@ namespace Umbraco.Cms.Core.Routing
umbracoContext,
siteDomainMapper,
StaticServiceProvider.Instance.GetRequiredService<IDomainCache>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedCache>(),
StaticServiceProvider.Instance.GetRequiredService<INavigationQueryService>());
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
[Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")]
public static string? GetCultureFromDomains(
int contentId,
string contentPath,
Uri? current,
IUmbracoContext umbracoContext,
ISiteDomainMapper siteDomainMapper,
IDomainCache domainCache,
IPublishedCache publishedCache,
INavigationQueryService navigationQueryService)
=> GetCultureFromDomains(
contentId,
contentPath,
current,
umbracoContext,
siteDomainMapper,
domainCache,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
/// <summary>
/// Gets the culture assigned to a document by domains, in the context of a current Uri.
@@ -57,8 +63,8 @@ namespace Umbraco.Cms.Core.Routing
/// <param name="umbracoContext">An Umbraco context.</param>
/// <param name="siteDomainMapper">The site domain helper.</param>
/// <param name="domainCache">The domain cache.</param>
/// <param name="publishedCache">The published content cache.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService"></param>
/// <returns>The culture assigned to the document by domains.</returns>
/// <remarks>
/// <para>In 1:1 multilingual setup, a document contains several cultures (there is not
@@ -72,8 +78,8 @@ namespace Umbraco.Cms.Core.Routing
IUmbracoContext umbracoContext,
ISiteDomainMapper siteDomainMapper,
IDomainCache domainCache,
IPublishedCache publishedCache,
INavigationQueryService navigationQueryService)
INavigationQueryService navigationQueryService,
IPublishedStatusFilteringService publishedStatusFilteringService)
{
if (umbracoContext == null)
{
@@ -85,7 +91,7 @@ namespace Umbraco.Cms.Core.Routing
current = umbracoContext.CleanedUmbracoUrl;
}
var domainNodeId = GetAncestorNodeWithDomainsAssigned(contentId, umbracoContext, domainCache, publishedCache, navigationQueryService);
var domainNodeId = GetAncestorNodeWithDomainsAssigned(contentId, umbracoContext, domainCache, navigationQueryService, publishedStatusFilteringService);
DomainAndUri? domain = domainNodeId.HasValue
? DomainForNode(umbracoContext.Domains, siteDomainMapper, domainNodeId.Value, current)
@@ -107,13 +113,13 @@ namespace Umbraco.Cms.Core.Routing
return umbracoContext.Domains?.DefaultCulture;
}
private static int? GetAncestorNodeWithDomainsAssigned(int contentId, IUmbracoContext umbracoContext, IDomainCache domainCache, IPublishedCache publishedCache, INavigationQueryService navigationQueryService)
private static int? GetAncestorNodeWithDomainsAssigned(int contentId, IUmbracoContext umbracoContext, IDomainCache domainCache, INavigationQueryService navigationQueryService, IPublishedStatusFilteringService publishedStatusFilteringService)
{
IPublishedContent? content = umbracoContext.Content.GetById(contentId);
var hasDomains = ContentHasAssignedDomains(content, domainCache);
while (content is not null && !hasDomains)
{
content = content.Parent<IPublishedContent>(publishedCache, navigationQueryService);
content = content.Parent<IPublishedContent>(navigationQueryService, publishedStatusFilteringService);
hasDomains = content is not null && domainCache.HasAssigned(content.Id, true);
}

View File

@@ -1,7 +1,9 @@
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
@@ -23,6 +25,7 @@ public class NewDefaultUrlProvider : IUrlProvider
private readonly IIdKeyMap _idKeyMap;
private readonly IDocumentUrlService _documentUrlService;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
private readonly ILocalizedTextService? _localizedTextService;
private readonly ILogger<DefaultUrlProvider> _logger;
private readonly ISiteDomainMapper _siteDomainMapper;
@@ -41,7 +44,8 @@ public class NewDefaultUrlProvider : IUrlProvider
IDomainCache domainCache,
IIdKeyMap idKeyMap,
IDocumentUrlService documentUrlService,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_requestSettings = requestSettings.CurrentValue;
_logger = logger;
@@ -54,10 +58,40 @@ public class NewDefaultUrlProvider : IUrlProvider
_idKeyMap = idKeyMap;
_documentUrlService = documentUrlService;
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
requestSettings.OnChange(x => _requestSettings = x);
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public NewDefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
ISiteDomainMapper siteDomainMapper,
IUmbracoContextAccessor umbracoContextAccessor,
UriUtility uriUtility,
ILocalizationService localizationService,
IPublishedContentCache publishedContentCache,
IDomainCache domainCache,
IIdKeyMap idKeyMap,
IDocumentUrlService documentUrlService,
IDocumentNavigationQueryService navigationQueryService)
: this(
requestSettings,
logger,
siteDomainMapper,
umbracoContextAccessor,
uriUtility,
localizationService,
publishedContentCache,
domainCache,
idKeyMap,
documentUrlService,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
#region GetOtherUrls
/// <summary>
@@ -97,7 +131,7 @@ public class NewDefaultUrlProvider : IUrlProvider
// n is null at root
while (domainUris == null && n != null)
{
n = n.Parent<IPublishedContent>(_publishedContentCache, _navigationQueryService); // move to parent node
n = n.Parent<IPublishedContent>(_navigationQueryService, _publishedContentStatusFilteringService); // move to parent node
domainUris = n == null
? null
: DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current);

View File

@@ -25,9 +25,49 @@ namespace Umbraco.Cms.Core.Routing
/// <param name="urlProviders">The list of URL providers.</param>
/// <param name="mediaUrlProviders">The list of media URL providers.</param>
/// <param name="variationContextAccessor">The current variation accessor.</param>
/// <param name="contentCache">The content cache.</param>
/// <param name="navigationQueryService">The query service for the in-memory navigation structure.</param>
/// <param name="publishStatusQueryService">The publish status query service, to query if a given content is published in a given culture.</param>
/// <param name="publishedContentStatusFilteringService"></param>
public UrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
IOptions<WebRoutingSettings> routingSettings,
UrlProviderCollection urlProviders,
MediaUrlProviderCollection mediaUrlProviders,
IVariationContextAccessor variationContextAccessor,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_urlProviders = urlProviders;
_mediaUrlProviders = mediaUrlProviders;
_variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor));
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
Mode = routingSettings.Value.UrlProviderMode;
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
IOptions<WebRoutingSettings> routingSettings,
UrlProviderCollection urlProviders,
MediaUrlProviderCollection mediaUrlProviders,
IVariationContextAccessor variationContextAccessor,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishStatusQueryService publishStatusQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
umbracoContextAccessor,
routingSettings,
urlProviders,
mediaUrlProviders,
variationContextAccessor,
navigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
IOptions<WebRoutingSettings> routingSettings,
@@ -37,18 +77,18 @@ namespace Umbraco.Cms.Core.Routing
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishStatusQueryService publishStatusQueryService)
: this(
umbracoContextAccessor,
routingSettings,
urlProviders,
mediaUrlProviders,
variationContextAccessor,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_urlProviders = urlProviders;
_mediaUrlProviders = mediaUrlProviders;
_variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor));
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_publishStatusQueryService = publishStatusQueryService;
Mode = routingSettings.Value.UrlProviderMode;
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
IOptions<WebRoutingSettings> routingSettings,
@@ -63,13 +103,12 @@ namespace Umbraco.Cms.Core.Routing
urlProviders,
mediaUrlProviders,
variationContextAccessor,
contentCache,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UrlProvider(
IUmbracoContextAccessor umbracoContextAccessor,
IOptions<WebRoutingSettings> routingSettings,
@@ -82,9 +121,8 @@ namespace Umbraco.Cms.Core.Routing
urlProviders,
mediaUrlProviders,
variationContextAccessor,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
@@ -92,9 +130,8 @@ namespace Umbraco.Cms.Core.Routing
private readonly IEnumerable<IUrlProvider> _urlProviders;
private readonly IEnumerable<IMediaUrlProvider> _mediaUrlProviders;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
/// <summary>
/// Gets or sets the provider URL mode.
@@ -173,7 +210,7 @@ namespace Umbraco.Cms.Core.Routing
// be nice with tests, assume things can be null, ultimately fall back to invariant
// (but only for variant content of course)
// We need to check all ancestors because urls are variant even for invariant content, if an ancestor is variant.
if (culture == null && content.AncestorsOrSelf(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService).Any(x => x.ContentType.VariesByCulture()))
if (culture == null && content.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).Any(x => x.ContentType.VariesByCulture()))
{
culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty;
}

View File

@@ -14,7 +14,7 @@ namespace Umbraco.Extensions;
public static class UrlProviderExtensions
{
[Obsolete("Use GetContentUrlsAsync that takes all parameters. Will be removed in V17.")]
[Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")]
public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
this IContent content,
IPublishedRouter publishedRouter,
@@ -36,8 +36,35 @@ public static class UrlProviderExtensions
logger,
uriUtility,
publishedUrlProvider,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>());
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
[Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")]
public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
this IContent content,
IPublishedRouter publishedRouter,
IUmbracoContext umbracoContext,
ILanguageService languageService,
ILocalizedTextService textService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILogger<IContent> logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
=> await content.GetContentUrlsAsync(
publishedRouter,
umbracoContext,
languageService,
textService,
contentService,
variationContextAccessor,
logger,
uriUtility,
publishedUrlProvider,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>());
/// <summary>
/// Gets the URLs of the content item.
@@ -57,8 +84,8 @@ public static class UrlProviderExtensions
ILogger<IContent> logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(publishedRouter);
@@ -95,7 +122,7 @@ public static class UrlProviderExtensions
// get all URLs for all cultures
// in a HashSet, so de-duplicates too
foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider, contentCache, navigationQueryService))
foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider, navigationQueryService, publishedContentStatusFilteringService))
{
urls.Add(cultureUrl);
}
@@ -146,8 +173,8 @@ public static class UrlProviderExtensions
ILogger logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
var result = new List<UrlInfo>();
@@ -186,7 +213,7 @@ public static class UrlProviderExtensions
// got a URL, deal with collisions, add URL
default:
// detect collisions, etc
Attempt<UrlInfo?> hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, contentCache, navigationQueryService);
Attempt<UrlInfo?> hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, navigationQueryService, publishedContentStatusFilteringService);
if (hasCollision.Success && hasCollision.Result is not null)
{
result.Add(hasCollision.Result);
@@ -243,8 +270,8 @@ public static class UrlProviderExtensions
ILocalizedTextService textService,
IVariationContextAccessor variationContextAccessor,
UriUtility uriUtility,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
// test for collisions on the 'main' URL
var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute);
@@ -283,7 +310,7 @@ public static class UrlProviderExtensions
while (o != null)
{
l.Add(o.Name(variationContextAccessor)!);
o = o.Parent<IPublishedContent>(contentCache, navigationQueryService);
o = o.Parent<IPublishedContent>(navigationQueryService, publishedContentStatusFilteringService);
}
l.Reverse();

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IPublishedContentStatusFilteringService : IPublishedStatusFilteringService
{
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IPublishedMediaStatusFilteringService : IPublishedStatusFilteringService
{
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IPublishedStatusFilteringService
{
IEnumerable<IPublishedContent> FilterAvailable(IEnumerable<Guid> candidateKeys, string? culture);
}

View File

@@ -0,0 +1,50 @@
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.Navigation;
internal sealed class PublishedContentStatusFilteringService : IPublishedContentStatusFilteringService
{
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly IPreviewService _previewService;
private readonly IPublishedContentCache _publishedContentCache;
public PublishedContentStatusFilteringService(
IVariationContextAccessor variationContextAccessor,
IPublishStatusQueryService publishStatusQueryService,
IPreviewService previewService,
IPublishedContentCache publishedContentCache)
{
_variationContextAccessor = variationContextAccessor;
_publishStatusQueryService = publishStatusQueryService;
_previewService = previewService;
_publishedContentCache = publishedContentCache;
}
public IEnumerable<IPublishedContent> FilterAvailable(IEnumerable<Guid> candidateKeys, string? culture)
{
culture ??= _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
Guid[] candidateKeysAsArray = candidateKeys as Guid[] ?? candidateKeys.ToArray();
if (candidateKeysAsArray.Length == 0)
{
return [];
}
var preview = _previewService.IsInPreview();
candidateKeys = preview
? candidateKeysAsArray
: candidateKeysAsArray.Where(key => _publishStatusQueryService.IsDocumentPublished(key, culture));
return WhereIsInvariantOrHasCulture(candidateKeys, culture, preview).ToArray();
}
private IEnumerable<IPublishedContent> WhereIsInvariantOrHasCulture(IEnumerable<Guid> keys, string culture, bool preview)
=> keys
.Select(key => _publishedContentCache.GetById(preview, key))
.WhereNotNull()
.Where(content => content.ContentType.VariesByCulture() is false
|| content.Cultures.ContainsKey(culture));
}

View File

@@ -0,0 +1,19 @@
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.Navigation;
// NOTE: this class is basically a no-op implementation of IPublishStatusQueryService, because the published
// content extensions need a media equivalent to the content implementation.
// incidentally, if we'll ever support variant and/or draft media, this comes in really handy :-)
internal sealed class PublishedMediaStatusFilteringService : IPublishedMediaStatusFilteringService
{
private readonly IPublishedMediaCache _publishedMediaCache;
public PublishedMediaStatusFilteringService(IPublishedMediaCache publishedMediaCache)
=> _publishedMediaCache = publishedMediaCache;
public IEnumerable<IPublishedContent> FilterAvailable(IEnumerable<Guid> candidateKeys, string? culture)
=> candidateKeys.Select(_publishedMediaCache.GetById).WhereNotNull().ToArray();
}

View File

@@ -1,27 +1,68 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Routing
{
internal class RedirectTracker : IRedirectTracker
{
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ILocalizationService _localizationService;
private readonly IRedirectUrlService _redirectUrlService;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly ILogger<RedirectTracker> _logger;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
public RedirectTracker(
ILocalizationService localizationService,
IRedirectUrlService redirectUrlService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
ILogger<RedirectTracker> logger,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
{
_localizationService = localizationService;
_redirectUrlService = redirectUrlService;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_logger = logger;
_publishedUrlProvider = publishedUrlProvider;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public RedirectTracker(
IVariationContextAccessor variationContextAccessor,
ILocalizationService localizationService,
IRedirectUrlService redirectUrlService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
ILogger<RedirectTracker> logger,
IPublishedUrlProvider publishedUrlProvider,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
localizationService,
redirectUrlService,
contentCache,
navigationQueryService,
logger,
publishedUrlProvider,
publishedContentStatusFilteringService)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public RedirectTracker(
IVariationContextAccessor variationContextAccessor,
ILocalizationService localizationService,
@@ -30,14 +71,15 @@ namespace Umbraco.Cms.Infrastructure.Routing
IDocumentNavigationQueryService navigationQueryService,
ILogger<RedirectTracker> logger,
IPublishedUrlProvider publishedUrlProvider)
: this(
localizationService,
redirectUrlService,
contentCache,
navigationQueryService,
logger,
publishedUrlProvider,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
_variationContextAccessor = variationContextAccessor;
_localizationService = localizationService;
_redirectUrlService = redirectUrlService;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_logger = logger;
_publishedUrlProvider = publishedUrlProvider;
}
/// <inheritdoc/>
@@ -50,12 +92,12 @@ namespace Umbraco.Cms.Infrastructure.Routing
}
// Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
var defaultCultures = new Lazy<string[]>(() => entityContent.AncestorsOrSelf(_contentCache, _navigationQueryService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty<string>());
var defaultCultures = new Lazy<string[]>(() => entityContent.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty<string>());
// Get all language ISO codes (in case we're dealing with invariant content with variant ancestors)
var languageIsoCodes = new Lazy<string[]>(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray());
foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _navigationQueryService))
foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService))
{
// If this entity defines specific cultures, use those instead of the default ones
IEnumerable<string> cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value;

View File

@@ -270,22 +270,22 @@ internal class PublishedContent : PublishedContentBase
private IPublishedContent? GetParent()
{
INavigationQueryService? navigationQueryService;
IPublishedCache? publishedCache;
IPublishedStatusFilteringService? publishedStatusFilteringService;
switch (ContentType.ItemType)
{
case PublishedItemType.Content:
publishedCache = StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>();
navigationQueryService = StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>();
publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>();
break;
case PublishedItemType.Media:
publishedCache = StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaCache>();
navigationQueryService = StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationQueryService>();
publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaStatusFilteringService>();
break;
default:
throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType);
}
return this.Parent<IPublishedContent>(publishedCache, navigationQueryService);
return this.Parent<IPublishedContent>(navigationQueryService, publishedStatusFilteringService);
}
}

View File

@@ -25,9 +25,6 @@ public static class FriendlyPublishedContentExtensions
private static IPublishedContentCache PublishedContentCache { get; } =
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>();
private static IPublishedMediaCache PublishedMediaCache { get; } =
StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaCache>();
private static IDocumentNavigationQueryService DocumentNavigationQueryService { get; } =
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>();
@@ -70,9 +67,6 @@ public static class FriendlyPublishedContentExtensions
private static IMemberTypeService MemberTypeService { get; } =
StaticServiceProvider.Instance.GetRequiredService<IMemberTypeService>();
private static IPublishStatusQueryService PublishStatusQueryService { get; } =
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>();
private static INavigationQueryService GetNavigationQueryService(IPublishedContent content)
{
switch (content.ContentType.ItemType)
@@ -84,17 +78,16 @@ public static class FriendlyPublishedContentExtensions
default:
throw new NotSupportedException("Unsupported content type.");
}
}
private static IPublishedCache GetPublishedCache(IPublishedContent content)
private static IPublishedStatusFilteringService GetPublishedStatusFilteringService(IPublishedContent content)
{
switch (content.ContentType.ItemType)
{
case PublishedItemType.Content:
return PublishedContentCache;
return StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>();
case PublishedItemType.Media:
return PublishedMediaCache;
return StaticServiceProvider.Instance.GetRequiredService<IPublishedMediaStatusFilteringService>();
default:
throw new NotSupportedException("Unsupported content type.");
}
@@ -246,7 +239,7 @@ public static class FriendlyPublishedContentExtensions
/// set to 1.
/// </remarks>
public static IPublishedContent Root(this IPublishedContent content)
=> content.Root(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.Root(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the root content (ancestor or self at level 1) for the specified <paramref name="content" /> if it's of the
@@ -265,7 +258,7 @@ public static class FriendlyPublishedContentExtensions
/// </remarks>
public static T? Root<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.Root<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.Root<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the parent of the content item.
@@ -275,7 +268,7 @@ public static class FriendlyPublishedContentExtensions
/// <returns>The parent of content of the specified content type or <c>null</c>.</returns>
public static T? Parent<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.Parent<T>(GetPublishedCache(content), GetNavigationQueryService(content));
=> content.Parent<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the parent of the content item.
@@ -283,7 +276,7 @@ public static class FriendlyPublishedContentExtensions
/// <param name="content">The content.</param>
/// <returns>The parent of content or <c>null</c>.</returns>
public static IPublishedContent? Parent(this IPublishedContent content)
=> content.Parent<IPublishedContent>(GetPublishedCache(content), GetNavigationQueryService(content));
=> content.Parent<IPublishedContent>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the ancestors of the content.
@@ -292,7 +285,7 @@ public static class FriendlyPublishedContentExtensions
/// <returns>The ancestors of the content, in down-top order.</returns>
/// <remarks>Does not consider the content itself.</remarks>
public static IEnumerable<IPublishedContent> Ancestors(this IPublishedContent content)
=> content.Ancestors(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the content and its ancestors.
@@ -300,7 +293,7 @@ public static class FriendlyPublishedContentExtensions
/// <param name="content">The content.</param>
/// <returns>The content and its ancestors, in down-top order.</returns>
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content)
=> content.AncestorsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the content and its ancestors, of a specified content type.
@@ -311,7 +304,7 @@ public static class FriendlyPublishedContentExtensions
/// <remarks>May or may not begin with the content itself, depending on its content type.</remarks>
public static IEnumerable<T> AncestorsOrSelf<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.AncestorsOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.AncestorsOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the ancestor of the content, i.e. its parent.
@@ -319,7 +312,7 @@ public static class FriendlyPublishedContentExtensions
/// <param name="content">The content.</param>
/// <returns>The ancestor of the content.</returns>
public static IPublishedContent? Ancestor(this IPublishedContent content)
=> content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content));
=> content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the nearest ancestor of the content, of a specified content type.
@@ -330,7 +323,7 @@ public static class FriendlyPublishedContentExtensions
/// <remarks>Does not consider the content itself. May return <c>null</c>.</remarks>
public static T? Ancestor<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.Ancestor<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.Ancestor<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the content or its nearest ancestor, of a specified content type.
@@ -341,7 +334,7 @@ public static class FriendlyPublishedContentExtensions
/// <remarks>May or may not return the content itself depending on its content type. May return <c>null</c>.</remarks>
public static T? AncestorOrSelf<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.AncestorOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService);
=> content.AncestorOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Returns all DescendantsOrSelf of all content referenced
@@ -358,7 +351,19 @@ public static class FriendlyPublishedContentExtensions
/// </remarks>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IEnumerable<IPublishedContent> parentNodes, string docTypeAlias, string? culture = null)
=> parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, GetPublishedCache(parentNodes.First()), GetNavigationQueryService(parentNodes.First()), PublishStatusQueryService, docTypeAlias, culture);
{
IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray();
if (parentNodesAsArray.Length == 0)
{
return [];
}
return parentNodesAsArray.DescendantsOrSelfOfType(
GetNavigationQueryService(parentNodesAsArray.First()),
GetPublishedStatusFilteringService(parentNodesAsArray.First()),
docTypeAlias,
culture);
}
/// <summary>
/// Returns all DescendantsOrSelf of all content referenced
@@ -376,77 +381,88 @@ public static class FriendlyPublishedContentExtensions
this IEnumerable<IPublishedContent> parentNodes,
string? culture = null)
where T : class, IPublishedContent
=> parentNodes.DescendantsOrSelf<T>(VariationContextAccessor, GetPublishedCache(parentNodes.First()), GetNavigationQueryService(parentNodes.First()), PublishStatusQueryService, culture);
{
IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray();
if (parentNodesAsArray.Length == 0)
{
return [];
}
return parentNodesAsArray.DescendantsOrSelf<T>(
GetNavigationQueryService(parentNodesAsArray.First()),
GetPublishedStatusFilteringService(parentNodesAsArray.First()),
culture);
}
public static IEnumerable<IPublishedContent> Descendants(this IPublishedContent content, string? culture = null)
=> content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IEnumerable<IPublishedContent> Descendants(this IPublishedContent content, int level, string? culture = null)
=> content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IEnumerable<IPublishedContent> DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.DescendantsOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.DescendantsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
public static IEnumerable<T> Descendants<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.Descendants<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Descendants<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IEnumerable<T> Descendants<T>(this IPublishedContent content, int level, string? culture = null)
where T : class, IPublishedContent
=> content.Descendants<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.Descendants<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IEnumerable<IPublishedContent> DescendantsOrSelf(
this IPublishedContent content,
string? culture = null)
=> content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IEnumerable<IPublishedContent> DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null)
=> content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.DescendantsOrSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.DescendantsOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
public static IEnumerable<T> DescendantsOrSelf<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.DescendantsOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.DescendantsOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IEnumerable<T> DescendantsOrSelf<T>(this IPublishedContent content, int level, string? culture = null)
where T : class, IPublishedContent
=> content.DescendantsOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.DescendantsOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IPublishedContent? Descendant(this IPublishedContent content, string? culture = null)
=> content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IPublishedContent? Descendant(this IPublishedContent content, int level, string? culture = null)
=> content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IPublishedContent? DescendantOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.DescendantOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.DescendantOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
public static T? Descendant<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.Descendant<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Descendant<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static T? Descendant<T>(this IPublishedContent content, int level, string? culture = null)
where T : class, IPublishedContent
=> content.Descendant<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.Descendant<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string? culture = null)
=> content.DescendantOrSelf(VariationContextAccessor, PublishStatusQueryService, culture);
=> content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null)
=> content.DescendantOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.DescendantOrSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.DescendantOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
public static T? DescendantOrSelf<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.DescendantOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.DescendantOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static T? DescendantOrSelf<T>(this IPublishedContent content, int level, string? culture = null)
where T : class, IPublishedContent
=> content.DescendantOrSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture);
=> content.DescendantOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture);
/// <summary>
/// Gets the children of the content item.
@@ -474,7 +490,7 @@ public static class FriendlyPublishedContentExtensions
/// </para>
/// </remarks>
public static IEnumerable<IPublishedContent> Children(this IPublishedContent content, string? culture = null)
=> content.Children(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the children of the content, filtered by a predicate.
@@ -493,7 +509,7 @@ public static class FriendlyPublishedContentExtensions
this IPublishedContent content,
Func<IPublishedContent, bool> predicate,
string? culture = null)
=> content.Children(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture);
=> content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture);
/// <summary>
/// Gets the children of the content, of any of the specified types.
@@ -506,7 +522,7 @@ public static class FriendlyPublishedContentExtensions
/// <param name="contentTypeAlias">The content type alias.</param>
/// <returns>The children of the content, of any of the specified types.</returns>
public static IEnumerable<IPublishedContent>? ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.ChildrenOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.ChildrenOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
/// <summary>
/// Gets the children of the content, of a given content type.
@@ -523,30 +539,30 @@ public static class FriendlyPublishedContentExtensions
/// </remarks>
public static IEnumerable<T>? Children<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.Children<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Children<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static IPublishedContent? FirstChild(this IPublishedContent content, string? culture = null)
=> content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the first child of the content, of a given content type.
/// </summary>
public static IPublishedContent? FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.FirstChildOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.FirstChildOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
public static IPublishedContent? FirstChild(this IPublishedContent content, Func<IPublishedContent, bool> predicate, string? culture = null)
=> content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture);
=> content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture);
public static IPublishedContent? FirstChild(this IPublishedContent content, Guid uniqueId, string? culture = null)
=> content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, uniqueId, culture);
=> content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), uniqueId, culture);
public static T? FirstChild<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.FirstChild<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.FirstChild<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
public static T? FirstChild<T>(this IPublishedContent content, Func<T, bool> predicate, string? culture = null)
where T : class, IPublishedContent
=> content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture);
=> content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture);
/// <summary>
/// Gets the siblings of the content.
@@ -561,7 +577,7 @@ public static class FriendlyPublishedContentExtensions
/// <para>Note that in V7 this method also return the content node self.</para>
/// </remarks>
public static IEnumerable<IPublishedContent>? Siblings(this IPublishedContent content, string? culture = null)
=> content.Siblings(GetPublishedCache(content), GetNavigationQueryService(content), VariationContextAccessor, PublishStatusQueryService, culture);
=> content.Siblings(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the siblings of the content, of a given content type.
@@ -577,7 +593,7 @@ public static class FriendlyPublishedContentExtensions
/// <para>Note that in V7 this method also return the content node self.</para>
/// </remarks>
public static IEnumerable<IPublishedContent>? SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null)
=> content.SiblingsOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.SiblingsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
/// <summary>
/// Gets the siblings of the content, of a given content type.
@@ -594,7 +610,7 @@ public static class FriendlyPublishedContentExtensions
/// </remarks>
public static IEnumerable<T>? Siblings<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.Siblings<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.Siblings<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the siblings of the content including the node itself to indicate the position.
@@ -608,7 +624,7 @@ public static class FriendlyPublishedContentExtensions
public static IEnumerable<IPublishedContent>? SiblingsAndSelf(
this IPublishedContent content,
string? culture = null)
=> content.SiblingsAndSelf(GetPublishedCache(content), GetNavigationQueryService(content), VariationContextAccessor, PublishStatusQueryService, culture);
=> content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the siblings of the content including the node itself to indicate the position, of a given content type.
@@ -624,7 +640,7 @@ public static class FriendlyPublishedContentExtensions
this IPublishedContent content,
string contentTypeAlias,
string? culture = null)
=> content.SiblingsAndSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture);
=> content.SiblingsAndSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture);
/// <summary>
/// Gets the siblings of the content including the node itself to indicate the position, of a given content type.
@@ -638,7 +654,7 @@ public static class FriendlyPublishedContentExtensions
/// <returns>The siblings of the content including the node itself, of the given content type.</returns>
public static IEnumerable<T>? SiblingsAndSelf<T>(this IPublishedContent content, string? culture = null)
where T : class, IPublishedContent
=> content.SiblingsAndSelf<T>(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture);
=> content.SiblingsAndSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture);
/// <summary>
/// Gets the url of the content item.
@@ -671,11 +687,9 @@ public static class FriendlyPublishedContentExtensions
/// <returns>The children of the content.</returns>
[Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")]
public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string? culture = null)
=>
content.ChildrenAsTable(
VariationContextAccessor,
GetPublishedCache(content),
=> content.ChildrenAsTable(
GetNavigationQueryService(content),
GetPublishedStatusFilteringService(content),
ContentTypeService,
MediaTypeService,
MemberTypeService,

View File

@@ -17,22 +17,7 @@ public static class PublishedContentExtensions
{
#region Variations
/// <summary>
/// Gets the culture assigned to a document by domains, in the context of a current Uri.
/// </summary>
/// <param name="content">The document.</param>
/// <param name="umbracoContextAccessor"></param>
/// <param name="siteDomainHelper">The site domain helper.</param>
/// <param name="current">An optional current Uri.</param>
/// <returns>The culture assigned to the document by domains.</returns>
/// <remarks>
/// <para>
/// In 1:1 multilingual setup, a document contains several cultures (there is not
/// one document per culture), and domains, withing the context of a current Uri, assign
/// a culture to that document.
/// </para>
/// </remarks>
[Obsolete("Please use the method taking all parameters. This overload will be removed in V17.")]
[Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")]
public static string? GetCultureFromDomains(
this IPublishedContent content,
IUmbracoContextAccessor umbracoContextAccessor,
@@ -43,6 +28,19 @@ public static class PublishedContentExtensions
return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper);
}
public static string? GetCultureFromDomains(
this IPublishedContent content,
IUmbracoContextAccessor umbracoContextAccessor,
ISiteDomainMapper siteDomainHelper,
IDomainCache domainCache,
IPublishedCache publishedCache,
INavigationQueryService navigationQueryService,
Uri? current = null)
{
IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();
return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper, domainCache, publishedCache, navigationQueryService);
}
/// <summary>
/// Gets the culture assigned to a document by domains, in the context of a current Uri.
/// </summary>
@@ -68,6 +66,7 @@ public static class PublishedContentExtensions
IDomainCache domainCache,
IPublishedCache publishedCache,
INavigationQueryService navigationQueryService,
IPublishedStatusFilteringService publishedStatusFilteringService,
Uri? current = null)
{
IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext();

View File

@@ -23,8 +23,8 @@ public class UmbLoginController : SurfaceController
private readonly IMemberManager _memberManager;
private readonly IMemberSignInManager _signInManager;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService;
[ActivatorUtilitiesConstructor]
public UmbLoginController(
@@ -37,18 +37,75 @@ public class UmbLoginController : SurfaceController
IMemberSignInManager signInManager,
IMemberManager memberManager,
ITwoFactorLoginService twoFactorLoginService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_signInManager = signInManager;
_memberManager = memberManager;
_twoFactorLoginService = twoFactorLoginService;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UmbLoginController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager signInManager,
IMemberManager memberManager,
ITwoFactorLoginService twoFactorLoginService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
umbracoContextAccessor,
databaseFactory,
services,
appCaches,
profilingLogger,
publishedUrlProvider,
signInManager,
memberManager,
twoFactorLoginService,
navigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UmbLoginController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager signInManager,
IMemberManager memberManager,
ITwoFactorLoginService twoFactorLoginService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService)
: this(
umbracoContextAccessor,
databaseFactory,
services,
appCaches,
profilingLogger,
publishedUrlProvider,
signInManager,
memberManager,
twoFactorLoginService,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")]
public UmbLoginController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
@@ -69,8 +126,8 @@ public class UmbLoginController : SurfaceController
signInManager,
memberManager,
twoFactorLoginService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentCache>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
@@ -102,7 +159,7 @@ public class UmbLoginController : SurfaceController
// If it's not a local URL we'll redirect to the root of the current site.
return Redirect(Url.IsLocalUrl(model.RedirectUrl)
? model.RedirectUrl
: CurrentPage!.AncestorOrSelf(_contentCache, _navigationQueryService, 1)!.Url(PublishedUrlProvider));
: CurrentPage!.AncestorOrSelf(_navigationQueryService, _publishedContentStatusFilteringService, 1)!.Url(PublishedUrlProvider));
}
// Redirect to current URL by default.

View File

@@ -281,4 +281,47 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT
Assert.AreEqual(3, allSiblingsList.Count);
});
}
// a lot of structural querying assumes a specific order of descendants, so let's ensure that.
[Test]
public void Descendants_Are_In_Top_Down_Order_Of_Structure()
{
var result = DocumentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(Root.Key, out IEnumerable<Guid> descendantsKeys);
Assert.IsTrue(result);
var descendantsKeysAsArray = descendantsKeys.ToArray();
Assert.AreEqual(9, descendantsKeysAsArray.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(Root.Key, descendantsKeysAsArray[0]);
Assert.AreEqual(Child1.Key, descendantsKeysAsArray[1]);
Assert.AreEqual(Grandchild1.Key, descendantsKeysAsArray[2]);
Assert.AreEqual(Grandchild2.Key, descendantsKeysAsArray[3]);
Assert.AreEqual(Child2.Key, descendantsKeysAsArray[4]);
Assert.AreEqual(Grandchild3.Key, descendantsKeysAsArray[5]);
Assert.AreEqual(GreatGrandchild1.Key, descendantsKeysAsArray[6]);
Assert.AreEqual(Child3.Key, descendantsKeysAsArray[7]);
Assert.AreEqual(Grandchild4.Key, descendantsKeysAsArray[8]);
});
}
// a lot of structural querying assumes a specific order of ancestors, so let's ensure that.
[Test]
public void Ancestors_Are_In_Down_Top_Order()
{
var result = DocumentNavigationQueryService.TryGetAncestorsOrSelfKeys(GreatGrandchild1.Key, out IEnumerable<Guid> ancestorsKeys);
Assert.IsTrue(result);
var ancestorKeysAsArray = ancestorsKeys.ToArray();
Assert.AreEqual(4, ancestorKeysAsArray.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(GreatGrandchild1.Key, ancestorKeysAsArray[0]);
Assert.AreEqual(Grandchild3.Key, ancestorKeysAsArray[1]);
Assert.AreEqual(Child2.Key, ancestorKeysAsArray[2]);
Assert.AreEqual(Root.Key, ancestorKeysAsArray[3]);
});
}
}

View File

@@ -413,6 +413,6 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest
GetRequiredService<ILogger<IContent>>(),
GetRequiredService<UriUtility>(),
GetRequiredService<IPublishedUrlProvider>(),
GetRequiredService<IPublishedContentCache>(),
GetRequiredService<IDocumentNavigationQueryService>()).GetAwaiter().GetResult();
GetRequiredService<IDocumentNavigationQueryService>(),
GetRequiredService<IPublishedContentStatusFilteringService>()).GetAwaiter().GetResult();
}

View File

@@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
@@ -36,7 +36,11 @@ public class ContentBuilderTests : DeliveryApiTests
.Setup(p => p.GetContentPath(It.IsAny<IPublishedContent>(), It.IsAny<string?>()))
.Returns((IPublishedContent c, string? culture) => $"url:{c.UrlSegment}");
var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings());
var navigationQueryServiceMock = new Mock<IDocumentNavigationQueryService>();
IEnumerable<Guid> ancestorsKeys = [];
navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(key, out ancestorsKeys)).Returns(true);
var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings(), navigationQueryService: navigationQueryServiceMock.Object);
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor());
var result = builder.Build(content.Object);

View File

@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
@@ -26,7 +27,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var rootKey = Guid.NewGuid();
var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object);
var result = builder.Build(root);
Assert.IsNotNull(result);
Assert.AreEqual("/", result.Path);
@@ -47,13 +48,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache);
var result = builder.Build(child);
Assert.IsNotNull(result);
Assert.AreEqual("/the-child", result.Path);
@@ -77,14 +78,14 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), grandchild.Key)).Returns(grandchild);
IEnumerable<Guid> ancestorsKeys = [childKey, rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache);
var result = builder.Build(grandchild);
Assert.IsNotNull(result);
Assert.AreEqual("/the-child/the-grandchild", result.Path);
@@ -104,13 +105,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache);
var result = builder.Build(child, "en-us");
Assert.IsNotNull(result);
Assert.AreEqual("/the-child-en-us", result.Path);
@@ -136,13 +137,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache);
var result = builder.Build(child, "en-us");
Assert.IsNotNull(result);
Assert.AreEqual("/the-child", result.Path);
@@ -168,13 +169,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache);
var result = builder.Build(child, "en-us");
Assert.IsNotNull(result);
Assert.AreEqual("/the-child-en-us", result.Path);
@@ -197,7 +198,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var content = new Mock<IPublishedContent>();
content.SetupGet(c => c.ItemType).Returns(itemType);
var builder = CreateApiContentRouteBuilder(true);
var builder = CreateApiContentRouteBuilder(true, Mock.Of<IDocumentNavigationQueryService>());
Assert.Throws<ArgumentException>(() => builder.Build(content.Object));
}
@@ -236,9 +237,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child);
var contentCache = Mock.Of<IPublishedContentCache>();
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), grandchild.Key)).Returns(grandchild);
IEnumerable<Guid> grandchildAncestorsKeys = [childKey, rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out grandchildAncestorsKeys)).Returns(true);
@@ -262,17 +263,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var rootKey = Guid.NewGuid();
var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock);
IEnumerable<Guid> rootKeys = rootKey.Yield();
navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true);
var childKey = Guid.NewGuid();
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: true);
var result = builder.Build(child);
Assert.IsNotNull(result);
Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path);
@@ -289,17 +290,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var rootKey = Guid.NewGuid();
var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock);
IEnumerable<Guid> rootKeys = rootKey.Yield();
navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true);
var childKey = Guid.NewGuid();
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, addTrailingSlash, contentCache: contentCache, isPreview: true);
var result = builder.Build(child);
Assert.IsNotNull(result);
Assert.AreEqual(addTrailingSlash, result.Path.EndsWith("/"));
@@ -314,9 +315,6 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var rootKey = Guid.NewGuid();
var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock, published: false);
IEnumerable<Guid> rootKeys = rootKey.Yield();
navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true);
var childKey = Guid.NewGuid();
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
@@ -324,13 +322,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests
requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: isPreview);
var result = builder.Build(child);
if (isPreview)
@@ -358,8 +356,8 @@ public class ContentRouteBuilderTests : DeliveryApiTests
var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root);
var contentCache = CreatePublishedContentCache("#");
Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), root.Key)).Returns(root);
Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny<bool>(), child.Key)).Returns(child);
var apiContentPathProvider = new Mock<IApiContentPathProvider>();
apiContentPathProvider
@@ -369,7 +367,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests
IEnumerable<Guid> ancestorsKeys = [rootKey];
navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true);
var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object);
var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object);
var result = builder.Build(root);
Assert.NotNull(result);
Assert.AreEqual("/my-custom-path-for-the-root", result.Path);
@@ -432,7 +430,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests
string Url(IPublishedContent content, string? culture)
{
var ancestorsOrSelf = content.AncestorsOrSelf(variantContextAccessor, contentCache, navigationQueryService, PublishStatusQueryService).ToArray();
var publishedContentStatusFilteringService = new PublishedContentStatusFilteringService(
variantContextAccessor,
PublishStatusQueryService,
Mock.Of<IPreviewService>(),
contentCache);
var ancestorsOrSelf = content.AncestorsOrSelf(navigationQueryService, publishedContentStatusFilteringService).ToArray();
return ancestorsOrSelf.All(c => c.IsPublished(culture))
? string.Join("/", ancestorsOrSelf.Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/")
: "#";
@@ -448,7 +451,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests
private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService)
=> new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService));
private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null, IDocumentNavigationQueryService navigationQueryService = null)
private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, IDocumentNavigationQueryService navigationQueryService, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null)
{
var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash };
var requestHandlerSettingsMonitorMock = new Mock<IOptionsMonitor<RequestHandlerSettings>>();

View File

@@ -113,13 +113,14 @@ public class DeliveryApiTests
content.SetupGet(c => c.ContentType).Returns(contentType);
content.SetupGet(c => c.Properties).Returns(properties);
content.SetupGet(c => c.ItemType).Returns(contentType.ItemType);
content.SetupGet(c => c.Level).Returns(1);
content.Setup(c => c.IsPublished(It.IsAny<string?>())).Returns(true);
}
protected string DefaultUrlSegment(string name, string? culture = null)
=> $"{name.ToLowerInvariant().Replace(" ", "-")}{(culture.IsNullOrWhiteSpace() ? string.Empty : $"-{culture}")}";
protected ApiContentRouteBuilder CreateContentRouteBuilder(
protected virtual ApiContentRouteBuilder CreateContentRouteBuilder(
IApiContentPathProvider contentPathProvider,
IOptions<GlobalSettings> globalSettings,
IVariationContextAccessor? variationContextAccessor = null,

View File

@@ -21,7 +21,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest
var contentNameProvider = new ApiContentNameProvider();
var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider);
routeBuilder = routeBuilder ?? CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings());
routeBuilder ??= CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings());
return new MultiNodeTreePickerValueConverter(
Mock.Of<IUmbracoContextAccessor>(),
Mock.Of<IMemberService>(),

View File

@@ -1,9 +1,12 @@
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
@@ -31,6 +34,8 @@ public class PropertyValueConverterTests : DeliveryApiTests
protected VariationContext VariationContext { get; } = new();
protected Mock<IDocumentNavigationQueryService> DocumentNavigationQueryServiceMock { get; private set; }
[SetUp]
public override void Setup()
{
@@ -76,6 +81,10 @@ public class PropertyValueConverterTests : DeliveryApiTests
.Returns("the-media-url");
PublishedUrlProvider = PublishedUrlProviderMock.Object;
ApiContentPathProvider = new ApiContentPathProvider(PublishedUrlProvider);
DocumentNavigationQueryServiceMock = new Mock<IDocumentNavigationQueryService>();
IEnumerable<Guid> ancestorsKeys = [];
DocumentNavigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(contentKey, out ancestorsKeys)).Returns(true);
}
protected Mock<IPublishedContent> SetupPublishedContent(string name, Guid key, PublishedItemType itemType, IPublishedContentType contentType)
@@ -109,4 +118,28 @@ public class PropertyValueConverterTests : DeliveryApiTests
.Setup(pcc => pcc.GetById(It.IsAny<bool>(), media.Key))
.Returns(media);
}
protected override ApiContentRouteBuilder CreateContentRouteBuilder(
IApiContentPathProvider contentPathProvider,
IOptions<GlobalSettings> globalSettings,
IVariationContextAccessor? variationContextAccessor = null,
IRequestPreviewService? requestPreviewService = null,
IOptionsMonitor<RequestHandlerSettings>? requestHandlerSettingsMonitor = null,
IPublishedContentCache? contentCache = null,
IDocumentNavigationQueryService? navigationQueryService = null,
IPublishStatusQueryService? publishStatusQueryService = null)
{
contentCache ??= PublishedContentCacheMock.Object;
navigationQueryService ??= DocumentNavigationQueryServiceMock.Object;
return base.CreateContentRouteBuilder(
contentPathProvider,
globalSettings,
variationContextAccessor,
requestPreviewService,
requestHandlerSettingsMonitor,
contentCache,
navigationQueryService,
publishStatusQueryService);
}
}

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using AutoFixture.NUnit3;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
@@ -30,16 +31,14 @@ public class ContentFinderByUrlAliasTests
[Frozen] IPublishedContentCache publishedContentCache,
[Frozen] IUmbracoContextAccessor umbracoContextAccessor,
[Frozen] IUmbracoContext umbracoContext,
[Frozen] IVariationContextAccessor variationContextAccessor,
[Frozen] IPublishStatusQueryService publishStatusQueryService,
[Frozen] IDocumentNavigationQueryService documentNavigationQueryService,
[Frozen] IPublishedContentStatusFilteringService publishedContentStatusFilteringService,
IFileService fileService,
ContentFinderByUrlAlias sut,
IPublishedContent[] rootContents,
IPublishedProperty urlProperty)
{
// Arrange
var absoluteUrl = "http://localhost" + relativeUrl;
var variationContext = new VariationContext();
var contentItem = rootContents[0];
Mock.Get(umbracoContextAccessor).Setup(x => x.TryGetUmbracoContext(out umbracoContext)).Returns(true);
@@ -47,13 +46,22 @@ public class ContentFinderByUrlAliasTests
Mock.Get(publishedContentCache).Setup(x => x.GetAtRoot(null)).Returns(rootContents);
Mock.Get(contentItem).Setup(x => x.Id).Returns(nodeMatch);
Mock.Get(contentItem).Setup(x => x.GetProperty(Constants.Conventions.Content.UrlAlias)).Returns(urlProperty);
Mock.Get(contentItem).Setup(x => x.ItemType).Returns(PublishedItemType.Content);
Mock.Get(urlProperty).Setup(x => x.GetValue(null, null)).Returns(relativeUrl);
Mock.Get(variationContextAccessor).Setup(x => x.VariationContext).Returns(variationContext);
Mock.Get(publishStatusQueryService).Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>())).Returns(true);
IEnumerable<Guid> descendantKeys = [];
Mock.Get(documentNavigationQueryService).Setup(x => x.TryGetDescendantsKeys(It.IsAny<Guid>(), out descendantKeys)).Returns(true);
Mock.Get(publishedContentStatusFilteringService).Setup(x => x.FilterAvailable(It.IsAny<IEnumerable<Guid>>(), It.IsAny<string?>())).Returns([]);
var publishedRequestBuilder = new PublishedRequestBuilder(new Uri(absoluteUrl, UriKind.Absolute), fileService);
// Act
var sut = new ContentFinderByUrlAlias(
Mock.Of<ILogger<ContentFinderByUrlAlias>>(),
Mock.Of<IPublishedValueFallback>(),
umbracoContextAccessor,
documentNavigationQueryService,
publishedContentStatusFilteringService);
var result = await sut.TryFindContent(publishedRequestBuilder);
Assert.IsTrue(result);

View File

@@ -0,0 +1,357 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services.PublishStatus;
[TestFixture]
public partial class PublishedContentStatusFilteringServiceTests
{
[Test]
public void FilterAvailable_Invariant_ForNonPreview_YieldsPublishedItems()
{
var (sut, items) = SetupInvariant(false);
var children = sut.FilterAvailable(items.Keys, null).ToArray();
Assert.AreEqual(5, children.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(0, children[0].Id);
Assert.AreEqual(2, children[1].Id);
Assert.AreEqual(4, children[2].Id);
Assert.AreEqual(6, children[3].Id);
Assert.AreEqual(8, children[4].Id);
});
}
[Test]
public void FilterAvailable_Invariant_ForPreview_YieldsUnpublishedItems()
{
var (sut, items) = SetupInvariant(true);
var children = sut.FilterAvailable(items.Keys, null).ToArray();
Assert.AreEqual(10, children.Length);
for (var i = 0; i < 10; i++)
{
Assert.AreEqual(i, children[i].Id);
}
}
[TestCase("da-DK", 3)]
[TestCase("en-US", 4)]
public void FilterAvailable_Variant_ForNonPreview_YieldsPublishedItemsInCulture(string culture, int expectedNumberOfChildren)
{
var (sut, items) = SetupVariant(false, culture);
var children = sut.FilterAvailable(items.Keys, culture).ToArray();
Assert.AreEqual(expectedNumberOfChildren, children.Length);
// IDs 0 through 3 exist in both en-US and da-DK - only even IDs are published
Assert.Multiple(() =>
{
Assert.AreEqual(0, children[0].Id);
Assert.AreEqual(2, children[1].Id);
});
// IDs 4 through 6 exist only in en-US - only even IDs are published
if (culture == "en-US")
{
Assert.AreEqual(4, children[2].Id);
Assert.AreEqual(6, children[3].Id);
}
// IDs 7 through 9 exist only in da-DK - only even IDs are published
if (culture == "da-DK")
{
Assert.AreEqual(8, children[2].Id);
}
}
[TestCase("da-DK")]
[TestCase("en-US")]
public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(string culture)
{
var (sut, items) = SetupVariant(true, culture);
var children = sut.FilterAvailable(items.Keys, culture).ToArray();
Assert.AreEqual(7, children.Length);
// IDs 0 through 3 exist in both en-US and da-DK
Assert.Multiple(() =>
{
Assert.AreEqual(0, children[0].Id);
Assert.AreEqual(1, children[1].Id);
Assert.AreEqual(2, children[2].Id);
Assert.AreEqual(3, children[3].Id);
});
// IDs 4 through 6 exist only in en-US
if (culture == "en-US")
{
Assert.AreEqual(4, children[4].Id);
Assert.AreEqual(5, children[5].Id);
Assert.AreEqual(6, children[6].Id);
}
// IDs 7 through 9 exist only in da-DK
if (culture == "da-DK")
{
Assert.AreEqual(7, children[4].Id);
Assert.AreEqual(8, children[5].Id);
Assert.AreEqual(9, children[6].Id);
}
}
[TestCase("da-DK")]
[TestCase("en-US")]
public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCultureOrInvariant(string culture)
{
var (sut, items) = SetupMixedVariance(false, culture);
var children = sut.FilterAvailable(items.Keys, culture).ToArray();
Assert.AreEqual(4, children.Length);
// IDs 0 through 2 are invariant - only even IDs are published
Assert.Multiple(() =>
{
Assert.AreEqual(0, children[0].Id);
Assert.AreEqual(2, children[1].Id);
});
// IDs 3 through 5 exist in both en-US and da-DK - only even IDs are published
Assert.Multiple(() =>
{
Assert.AreEqual(4, children[2].Id);
});
// IDs 6 and 7 exist only in en-US - only even IDs are published
if (culture == "en-US")
{
Assert.AreEqual(6, children[3].Id);
}
// IDs 8 and 9 exist only in da-DK - only even IDs are published
if (culture == "da-DK")
{
Assert.AreEqual(8, children[3].Id);
}
}
[TestCase("da-DK")]
[TestCase("en-US")]
public void FilterAvailable_MixedVariance_FoPreview_YieldsPublishedItemsInCultureOrInvariant(string culture)
{
var (sut, items) = SetupMixedVariance(true, culture);
var children = sut.FilterAvailable(items.Keys, culture).ToArray();
Assert.AreEqual(8, children.Length);
// IDs 0 through 2 are invariant
Assert.Multiple(() =>
{
Assert.AreEqual(0, children[0].Id);
Assert.AreEqual(1, children[1].Id);
Assert.AreEqual(2, children[2].Id);
});
// IDs 3 through 5 exist in both en-US and da-DK
Assert.Multiple(() =>
{
Assert.AreEqual(3, children[3].Id);
Assert.AreEqual(4, children[4].Id);
Assert.AreEqual(5, children[5].Id);
});
// IDs 6 and 7 exist only in en-US
if (culture == "en-US")
{
Assert.AreEqual(6, children[6].Id);
Assert.AreEqual(7, children[7].Id);
}
// IDs 8 and 9 exist only in da-DK
if (culture == "da-DK")
{
Assert.AreEqual(8, children[6].Id);
Assert.AreEqual(9, children[7].Id);
}
}
// sets up invariant test data:
// - 10 documents with IDs 0 through 9
// - even IDs (0, 2, ...) are published, odd are unpublished
private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary<Guid, IPublishedContent> Items) SetupInvariant(bool forPreview)
{
var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
var items = new Dictionary<Guid, IPublishedContent>();
for (var i = 0; i < 10; i++)
{
var content = new Mock<IPublishedContent>();
var key = Guid.NewGuid();
content.SetupGet(c => c.Key).Returns(key);
content.SetupGet(c => c.ContentType).Returns(contentType.Object);
content.SetupGet(c => c.Cultures).Returns(new Dictionary<string, PublishedCultureInfo>());
content.SetupGet(c => c.Id).Returns(i);
items[key] = content.Object;
}
var publishedContentCache = SetupPublishedContentCache(forPreview, items);
var previewService = SetupPreviewService(forPreview);
var publishStatusQueryService = SetupPublishStatusQueryService(items);
var variationContextAccessor = SetupVariantContextAccessor(null);
return (
new PublishedContentStatusFilteringService(
variationContextAccessor,
publishStatusQueryService,
previewService,
publishedContentCache),
items);
}
// sets up variant test data:
// - 10 documents with IDs 0 through 9
// - IDs 0 through 3 exist in both en-US and da-DK
// - IDs 4 through 6 exist only in en-US
// - IDs 7 through 9 exist only in da-DK
// - even IDs (0, 2, ...) are published, odd are unpublished
private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary<Guid, IPublishedContent> Items) SetupVariant(bool forPreview, string requestCulture)
{
var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
var items = new Dictionary<Guid, IPublishedContent>();
for (var i = 0; i < 10; i++)
{
var content = new Mock<IPublishedContent>();
var key = Guid.NewGuid();
string[] cultures = i <= 3
? ["da-DK", "en-US"]
: i <= 6
? ["en-US"]
: ["da-DK"];
var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue));
content.SetupGet(c => c.Key).Returns(key);
content.SetupGet(c => c.ContentType).Returns(contentType.Object);
content.SetupGet(c => c.Cultures).Returns(cultureDictionary);
content.SetupGet(c => c.Id).Returns(i);
items[key] = content.Object;
}
var publishedContentCache = SetupPublishedContentCache(forPreview, items);
var previewService = SetupPreviewService(forPreview);
var publishStatusQueryService = SetupPublishStatusQueryService(items);
var variationContextAccessor = SetupVariantContextAccessor(requestCulture);
return (
new PublishedContentStatusFilteringService(
variationContextAccessor,
publishStatusQueryService,
previewService,
publishedContentCache),
items);
}
// sets up mixed variant test data:
// - 10 documents with IDs 0 through 9
// - IDs 0 through 2 are invariant
// - IDs 3 through 5 exist in both en-US and da-DK
// - IDs 6 and 7 exist only in en-US
// - IDs 8 and 9 exist only in da-DK
// - even IDs (0, 2, ...) are published, odd are unpublished
private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary<Guid, IPublishedContent> Items) SetupMixedVariance(bool forPreview, string requestCulture)
{
var invariantContentType = new Mock<IPublishedContentType>();
invariantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
var variantContentType = new Mock<IPublishedContentType>();
variantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
var items = new Dictionary<Guid, IPublishedContent>();
for (var i = 0; i < 10; i++)
{
var content = new Mock<IPublishedContent>();
var contentType = i <= 2
? invariantContentType
: variantContentType;
var key = Guid.NewGuid();
string[] cultures = i <= 2
? []
: i <= 5
? ["da-DK", "en-US"]
: i <= 7
? ["en-US"]
: ["da-DK"];
var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue));
content.SetupGet(c => c.Key).Returns(key);
content.SetupGet(c => c.ContentType).Returns(contentType.Object);
content.SetupGet(c => c.Cultures).Returns(cultureDictionary);
content.SetupGet(c => c.Id).Returns(i);
items[key] = content.Object;
}
var publishedContentCache = SetupPublishedContentCache(forPreview, items);
var previewService = SetupPreviewService(forPreview);
var publishStatusQueryService = SetupPublishStatusQueryService(items);
var variationContextAccessor = SetupVariantContextAccessor(requestCulture);
return (
new PublishedContentStatusFilteringService(
variationContextAccessor,
publishStatusQueryService,
previewService,
publishedContentCache),
items);
}
private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary<Guid, IPublishedContent> items)
=> SetupPublishStatusQueryService(items, id => id % 2 == 0);
private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary<Guid, IPublishedContent> items, Func<int, bool> idIsPublished)
{
var publishStatusQueryService = new Mock<IPublishStatusQueryService>();
publishStatusQueryService
.Setup(p => p.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns((Guid key, string culture) => items
.TryGetValue(key, out var item)
&& idIsPublished(item.Id)
&& (item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture)));
return publishStatusQueryService.Object;
}
private IPreviewService SetupPreviewService(bool forPreview)
{
var previewService = new Mock<IPreviewService>();
previewService.Setup(p => p.IsInPreview()).Returns(forPreview);
return previewService.Object;
}
private IVariationContextAccessor SetupVariantContextAccessor(string? requestCulture)
{
var variationContextAccessor = new Mock<IVariationContextAccessor>();
variationContextAccessor.SetupGet(v => v.VariationContext).Returns(new VariationContext(requestCulture));
return variationContextAccessor.Object;
}
private IPublishedContentCache SetupPublishedContentCache(bool forPreview, Dictionary<Guid, IPublishedContent> items)
{
var publishedContentCache = new Mock<IPublishedContentCache>();
publishedContentCache
.Setup(c => c.GetById(forPreview, It.IsAny<Guid>()))
.Returns((bool preview, Guid key) => items.TryGetValue(key, out var item) ? item : null);
return publishedContentCache.Object;
}
}

View File

@@ -98,9 +98,8 @@ public class HtmlImageSourceParserTests
new UrlProviderCollection(() => Enumerable.Empty<IUrlProvider>()),
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
Mock.Of<IVariationContextAccessor>(),
Mock.Of<IPublishedContentCache>(),
Mock.Of<IDocumentNavigationQueryService>(),
Mock.Of<IPublishStatusQueryService>());
Mock.Of<IPublishedContentStatusFilteringService>());
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
{

View File

@@ -222,8 +222,12 @@ public class HtmlLocalLinkParserTests
var webRoutingSettings = new WebRoutingSettings();
var navigationQueryService = new Mock<IDocumentNavigationQueryService>();
Guid? parentKey = null;
navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny<Guid>(), out parentKey)).Returns(true);
// Guid? parentKey = null;
// navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny<Guid>(), out parentKey)).Returns(true);
IEnumerable<Guid> ancestorKeys = [];
navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny<Guid>(), out ancestorKeys)).Returns(true);
var publishedContentStatusFilteringService = new Mock<IPublishedContentStatusFilteringService>();
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
{
@@ -246,9 +250,8 @@ public class HtmlLocalLinkParserTests
new UrlProviderCollection(() => new[] { contentUrlProvider.Object }),
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
Mock.Of<IVariationContextAccessor>(),
contentCache.Object,
navigationQueryService.Object,
publishStatusQueryService.Object);
publishedContentStatusFilteringService.Object);
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);