Added configuration option UseStrictDomainMatching, which allows control over whether content is routed without a matching domain (#19815)
* Added configuration option UseStrictDomainMatching, which allows control over whether content is routed without a matching domain. * Fixed typo in comment. * Addressed comments from code review.
This commit is contained in:
@@ -21,6 +21,7 @@ public class WebRoutingSettings
|
||||
internal const bool StaticDisableFindContentByIdentifierPath = false;
|
||||
internal const bool StaticDisableRedirectUrlTracking = false;
|
||||
internal const string StaticUrlProviderMode = "Auto";
|
||||
internal const bool StaticUseStrictDomainMatching = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before
|
||||
@@ -60,8 +61,12 @@ public class WebRoutingSettings
|
||||
[DefaultValue(StaticValidateAlternativeTemplates)]
|
||||
public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the content finder by a path of the content key (<see cref="Routing.ContentFinderByKeyPath" />) is disabled.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticDisableFindContentByIdentifierPath)]
|
||||
public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether redirect URL tracking is disabled.
|
||||
/// </summary>
|
||||
@@ -78,4 +83,15 @@ public class WebRoutingSettings
|
||||
/// Gets or sets a value for the Umbraco application URL.
|
||||
/// </summary>
|
||||
public string UmbracoApplicationUrl { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether strict domain matching is used when finding content to match the request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This setting is used within Umbraco's routing process based on content finders, specifically <see cref="Routing.ContentFinderByUrlNew" />.</para>
|
||||
/// <para>If set to the default value of <see langword="false"/>, requests that don't match a configured domain will be routed to the first root node.</para>
|
||||
/// <para>If set to <see langword="true"/>, requests that don't match a configured domain will not be routed.</para>
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticUseStrictDomainMatching)]
|
||||
public bool UseStrictDomainMatching { get; set; } = StaticUseStrictDomainMatching;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
@@ -19,6 +21,25 @@ public class ContentFinderByUrlNew : IContentFinder
|
||||
private readonly ILogger<ContentFinderByUrlNew> _logger;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private WebRoutingSettings _webRoutingSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public ContentFinderByUrlNew(
|
||||
ILogger<ContentFinderByUrlNew> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IPublishedContentCache publishedContentCache)
|
||||
: this(
|
||||
logger,
|
||||
umbracoContextAccessor,
|
||||
documentUrlService,
|
||||
publishedContentCache,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<WebRoutingSettings>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
|
||||
@@ -27,17 +48,20 @@ public class ContentFinderByUrlNew : IContentFinder
|
||||
ILogger<ContentFinderByUrlNew> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IPublishedContentCache publishedContentCache)
|
||||
IPublishedContentCache publishedContentCache,
|
||||
IOptionsMonitor<WebRoutingSettings> webRoutingSettings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_logger = logger;
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_documentUrlService = documentUrlService;
|
||||
UmbracoContextAccessor =
|
||||
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
UmbracoContextAccessor = umbracoContextAccessor;
|
||||
|
||||
_webRoutingSettings = webRoutingSettings.CurrentValue;
|
||||
webRoutingSettings.OnChange(x => _webRoutingSettings = x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IUmbracoContextAccessor" />
|
||||
/// Gets the <see cref="IUmbracoContextAccessor" />.
|
||||
/// </summary>
|
||||
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
|
||||
|
||||
@@ -61,6 +85,14 @@ public class ContentFinderByUrlNew : IContentFinder
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we have configured strict domain matching, and a domain has not been found for the request configured on an ancestor node,
|
||||
// do not route the content by URL.
|
||||
if (_webRoutingSettings.UseStrictDomainMatching)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Default behaviour if strict domain matching is not enabled will be to route under the to the first root node found.
|
||||
route = frequest.AbsolutePathDecoded;
|
||||
}
|
||||
|
||||
@@ -79,29 +111,24 @@ public class ContentFinderByUrlNew : IContentFinder
|
||||
return null;
|
||||
}
|
||||
|
||||
if (docreq == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(docreq));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(docreq);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Test route {Route}", route);
|
||||
}
|
||||
|
||||
var documentKey = _documentUrlService.GetDocumentKeyByRoute(
|
||||
docreq.Domain is null ? route : route.Substring(docreq.Domain.ContentId.ToString().Length),
|
||||
Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute(
|
||||
docreq.Domain is null ? route : route[docreq.Domain.ContentId.ToString().Length..],
|
||||
docreq.Culture,
|
||||
docreq.Domain?.ContentId,
|
||||
umbracoContext.InPreviewMode
|
||||
);
|
||||
umbracoContext.InPreviewMode);
|
||||
|
||||
|
||||
IPublishedContent? node = null;
|
||||
if (documentKey.HasValue)
|
||||
{
|
||||
node = _publishedContentCache.GetById(umbracoContext.InPreviewMode, documentKey.Value);
|
||||
//node = umbracoContext.Content?.GetById(umbracoContext.InPreviewMode, documentKey.Value);
|
||||
}
|
||||
|
||||
if (node != null)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.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.Web;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing;
|
||||
|
||||
[TestFixture]
|
||||
public class ContentFinderByUrlNewTests
|
||||
{
|
||||
private const int DomainContentId = 1233;
|
||||
private const int ContentId = 1234;
|
||||
private static readonly Guid _contentKey = Guid.NewGuid();
|
||||
private const string ContentPath = "/test-page";
|
||||
private const string DomainHost = "example.com";
|
||||
|
||||
[TestCase(ContentPath, true)]
|
||||
[TestCase("/missing-page", false)]
|
||||
public async Task Can_Find_Invariant_Content(string path, bool expectSuccess)
|
||||
{
|
||||
var mockContent = CreateMockPublishedContent();
|
||||
|
||||
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
|
||||
|
||||
var mockDocumentUrlService = CreateMockDocumentUrlService();
|
||||
|
||||
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
|
||||
|
||||
var sut = CreateContentFinder(mockUmbracoContextAccessor, mockDocumentUrlService, mockPublishedContentCache);
|
||||
|
||||
var publishedRequestBuilder = CreatePublishedRequestBuilder(path);
|
||||
|
||||
var result = await sut.TryFindContent(publishedRequestBuilder);
|
||||
|
||||
Assert.AreEqual(expectSuccess, result);
|
||||
if (expectSuccess)
|
||||
{
|
||||
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(publishedRequestBuilder.PublishedContent);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(ContentPath, true, false, true)]
|
||||
[TestCase("/missing-page", true, false, false)]
|
||||
[TestCase(ContentPath, true, true, true)]
|
||||
[TestCase(ContentPath, false, true, false)]
|
||||
public async Task Can_Find_Invariant_Content_With_Domain(string path, bool setDomain, bool useStrictDomainMatching, bool expectSuccess)
|
||||
{
|
||||
var mockContent = CreateMockPublishedContent();
|
||||
|
||||
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
|
||||
|
||||
var mockDocumentUrlService = CreateMockDocumentUrlService();
|
||||
|
||||
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
|
||||
|
||||
var sut = CreateContentFinder(
|
||||
mockUmbracoContextAccessor,
|
||||
mockDocumentUrlService,
|
||||
mockPublishedContentCache,
|
||||
new WebRoutingSettings
|
||||
{
|
||||
UseStrictDomainMatching = useStrictDomainMatching
|
||||
});
|
||||
|
||||
var publishedRequestBuilder = CreatePublishedRequestBuilder(path, withDomain: setDomain);
|
||||
|
||||
var result = await sut.TryFindContent(publishedRequestBuilder);
|
||||
|
||||
Assert.AreEqual(expectSuccess, result);
|
||||
if (expectSuccess)
|
||||
{
|
||||
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(publishedRequestBuilder.PublishedContent);
|
||||
}
|
||||
}
|
||||
|
||||
private static Mock<IPublishedContent> CreateMockPublishedContent()
|
||||
{
|
||||
var mockContent = new Mock<IPublishedContent>();
|
||||
mockContent
|
||||
.SetupGet(x => x.Id)
|
||||
.Returns(ContentId);
|
||||
mockContent
|
||||
.SetupGet(x => x.ContentType.ItemType)
|
||||
.Returns(PublishedItemType.Content);
|
||||
return mockContent;
|
||||
}
|
||||
|
||||
private static Mock<IUmbracoContextAccessor> CreateMockUmbracoContextAccessor()
|
||||
{
|
||||
var mockUmbracoContext = new Mock<IUmbracoContext>();
|
||||
var mockUmbracoContextAccessor = new Mock<IUmbracoContextAccessor>();
|
||||
var umbracoContext = mockUmbracoContext.Object;
|
||||
mockUmbracoContextAccessor
|
||||
.Setup(x => x.TryGetUmbracoContext(out umbracoContext))
|
||||
.Returns(true);
|
||||
return mockUmbracoContextAccessor;
|
||||
}
|
||||
|
||||
private static Mock<IDocumentUrlService> CreateMockDocumentUrlService()
|
||||
{
|
||||
var mockDocumentUrlService = new Mock<IDocumentUrlService>();
|
||||
mockDocumentUrlService
|
||||
.Setup(x => x.GetDocumentKeyByRoute(It.Is<string>(y => y == ContentPath), It.IsAny<string?>(), It.IsAny<int?>(), It.IsAny<bool>()))
|
||||
.Returns(_contentKey);
|
||||
return mockDocumentUrlService;
|
||||
}
|
||||
|
||||
private static Mock<IPublishedContentCache> CreateMockPublishedContentCache(Mock<IPublishedContent> mockContent)
|
||||
{
|
||||
var mockPublishedContentCache = new Mock<IPublishedContentCache>();
|
||||
mockPublishedContentCache
|
||||
.Setup(x => x.GetById(It.IsAny<bool>(), It.Is<Guid>(y => y == _contentKey)))
|
||||
.Returns(mockContent.Object);
|
||||
return mockPublishedContentCache;
|
||||
}
|
||||
|
||||
private static ContentFinderByUrlNew CreateContentFinder(
|
||||
Mock<IUmbracoContextAccessor> mockUmbracoContextAccessor,
|
||||
Mock<IDocumentUrlService> mockDocumentUrlService,
|
||||
Mock<IPublishedContentCache> mockPublishedContentCache,
|
||||
WebRoutingSettings? webRoutingSettings = null)
|
||||
=> new(
|
||||
new NullLogger<ContentFinderByUrlNew>(),
|
||||
mockUmbracoContextAccessor.Object,
|
||||
mockDocumentUrlService.Object,
|
||||
mockPublishedContentCache.Object,
|
||||
Mock.Of<IOptionsMonitor<WebRoutingSettings>>(x => x.CurrentValue == (webRoutingSettings ?? new WebRoutingSettings())));
|
||||
|
||||
private static PublishedRequestBuilder CreatePublishedRequestBuilder(string path, bool withDomain = false)
|
||||
{
|
||||
var publishedRequestBuilder = new PublishedRequestBuilder(new Uri($"https://example.com{path}"), Mock.Of<IFileService>());
|
||||
if (withDomain)
|
||||
{
|
||||
publishedRequestBuilder.SetDomain(new DomainAndUri(new Domain(1, $"https://{DomainHost}/", DomainContentId, "en-US", false, 0), new Uri($"https://{DomainHost}{path}")));
|
||||
}
|
||||
|
||||
return publishedRequestBuilder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user