diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
index 9c96f87c319c..e91074ee530e 100644
--- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs
@@ -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;
///
/// 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;
+ ///
+ /// Gets or sets a value indicating whether the content finder by a path of the content key () is disabled.
+ ///
[DefaultValue(StaticDisableFindContentByIdentifierPath)]
public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath;
+
///
/// Gets or sets a value indicating whether redirect URL tracking is disabled.
///
@@ -78,4 +83,15 @@ public class WebRoutingSettings
/// Gets or sets a value for the Umbraco application URL.
///
public string UmbracoApplicationUrl { get; set; } = null!;
+
+ ///
+ /// Gets or sets a value indicating whether strict domain matching is used when finding content to match the request.
+ ///
+ ///
+ /// This setting is used within Umbraco's routing process based on content finders, specifically .
+ /// If set to the default value of , requests that don't match a configured domain will be routed to the first root node.
+ /// If set to , requests that don't match a configured domain will not be routed.
+ ///
+ [DefaultValue(StaticUseStrictDomainMatching)]
+ public bool UseStrictDomainMatching { get; set; } = StaticUseStrictDomainMatching;
}
diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
index 76211530aa39..d788a7e3d4c1 100644
--- a/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
+++ b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
@@ -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,25 +21,47 @@ public class ContentFinderByUrlNew : IContentFinder
private readonly ILogger _logger;
private readonly IPublishedContentCache _publishedContentCache;
private readonly IDocumentUrlService _documentUrlService;
+ private WebRoutingSettings _webRoutingSettings;
///
/// Initializes a new instance of the class.
///
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
public ContentFinderByUrlNew(
ILogger logger,
IUmbracoContextAccessor umbracoContextAccessor,
IDocumentUrlService documentUrlService,
IPublishedContentCache publishedContentCache)
+ : this(
+ logger,
+ umbracoContextAccessor,
+ documentUrlService,
+ publishedContentCache,
+ StaticServiceProvider.Instance.GetRequiredService>())
{
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentFinderByUrlNew(
+ ILogger logger,
+ IUmbracoContextAccessor umbracoContextAccessor,
+ IDocumentUrlService documentUrlService,
+ IPublishedContentCache publishedContentCache,
+ IOptionsMonitor webRoutingSettings)
+ {
+ _logger = logger;
_publishedContentCache = publishedContentCache;
_documentUrlService = documentUrlService;
- UmbracoContextAccessor =
- umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
+ UmbracoContextAccessor = umbracoContextAccessor;
+
+ _webRoutingSettings = webRoutingSettings.CurrentValue;
+ webRoutingSettings.OnChange(x => _webRoutingSettings = x);
}
///
- /// Gets the
+ /// Gets the .
///
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
@@ -61,6 +85,14 @@ public virtual Task 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.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 virtual Task 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)
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlNewTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlNewTests.cs
new file mode 100644
index 000000000000..976a03f41f6d
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlNewTests.cs
@@ -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 CreateMockPublishedContent()
+ {
+ var mockContent = new Mock();
+ mockContent
+ .SetupGet(x => x.Id)
+ .Returns(ContentId);
+ mockContent
+ .SetupGet(x => x.ContentType.ItemType)
+ .Returns(PublishedItemType.Content);
+ return mockContent;
+ }
+
+ private static Mock CreateMockUmbracoContextAccessor()
+ {
+ var mockUmbracoContext = new Mock();
+ var mockUmbracoContextAccessor = new Mock();
+ var umbracoContext = mockUmbracoContext.Object;
+ mockUmbracoContextAccessor
+ .Setup(x => x.TryGetUmbracoContext(out umbracoContext))
+ .Returns(true);
+ return mockUmbracoContextAccessor;
+ }
+
+ private static Mock CreateMockDocumentUrlService()
+ {
+ var mockDocumentUrlService = new Mock();
+ mockDocumentUrlService
+ .Setup(x => x.GetDocumentKeyByRoute(It.Is(y => y == ContentPath), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(_contentKey);
+ return mockDocumentUrlService;
+ }
+
+ private static Mock CreateMockPublishedContentCache(Mock mockContent)
+ {
+ var mockPublishedContentCache = new Mock();
+ mockPublishedContentCache
+ .Setup(x => x.GetById(It.IsAny(), It.Is(y => y == _contentKey)))
+ .Returns(mockContent.Object);
+ return mockPublishedContentCache;
+ }
+
+ private static ContentFinderByUrlNew CreateContentFinder(
+ Mock mockUmbracoContextAccessor,
+ Mock mockDocumentUrlService,
+ Mock mockPublishedContentCache,
+ WebRoutingSettings? webRoutingSettings = null)
+ => new(
+ new NullLogger(),
+ mockUmbracoContextAccessor.Object,
+ mockDocumentUrlService.Object,
+ mockPublishedContentCache.Object,
+ Mock.Of>(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());
+ if (withDomain)
+ {
+ publishedRequestBuilder.SetDomain(new DomainAndUri(new Domain(1, $"https://{DomainHost}/", DomainContentId, "en-US", false, 0), new Uri($"https://{DomainHost}{path}")));
+ }
+
+ return publishedRequestBuilder;
+ }
+}