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; + } +}