Skip to content

Added configuration option UseStrictDomainMatching, which allows control over whether content is routed without a matching domain #19815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -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;
}
51 changes: 38 additions & 13 deletions src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,25 +21,45 @@ public class ContentFinderByUrlNew : IContentFinder
private readonly ILogger<ContentFinderByUrlNew> _logger;
private readonly IPublishedContentCache _publishedContentCache;
private readonly IDocumentUrlService _documentUrlService;
private readonly IOptionsMonitor<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>>())
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
/// </summary>
public ContentFinderByUrlNew(
ILogger<ContentFinderByUrlNew> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IDocumentUrlService documentUrlService,
IPublishedContentCache publishedContentCache,
IOptionsMonitor<WebRoutingSettings> webRoutingSettings)
{
_logger = logger;
_publishedContentCache = publishedContentCache;
_documentUrlService = documentUrlService;
UmbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
UmbracoContextAccessor = umbracoContextAccessor;
_webRoutingSettings = webRoutingSettings;
}

/// <summary>
/// Gets the <see cref="IUmbracoContextAccessor" />
/// Gets the <see cref="IUmbracoContextAccessor" />.
/// </summary>
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }

Expand All @@ -61,6 +83,14 @@ public virtual Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
}
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.CurrentValue.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;
}

Expand All @@ -79,29 +109,24 @@ public virtual Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}