Skip to content

Commit 2266529

Browse files
authored
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.
1 parent 4efe8f5 commit 2266529

File tree

3 files changed

+209
-13
lines changed

3 files changed

+209
-13
lines changed

src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class WebRoutingSettings
2121
internal const bool StaticDisableFindContentByIdentifierPath = false;
2222
internal const bool StaticDisableRedirectUrlTracking = false;
2323
internal const string StaticUrlProviderMode = "Auto";
24+
internal const bool StaticUseStrictDomainMatching = false;
2425

2526
/// <summary>
2627
/// 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
6061
[DefaultValue(StaticValidateAlternativeTemplates)]
6162
public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates;
6263

64+
/// <summary>
65+
/// Gets or sets a value indicating whether the content finder by a path of the content key (<see cref="Routing.ContentFinderByKeyPath" />) is disabled.
66+
/// </summary>
6367
[DefaultValue(StaticDisableFindContentByIdentifierPath)]
6468
public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath;
69+
6570
/// <summary>
6671
/// Gets or sets a value indicating whether redirect URL tracking is disabled.
6772
/// </summary>
@@ -78,4 +83,15 @@ public class WebRoutingSettings
7883
/// Gets or sets a value for the Umbraco application URL.
7984
/// </summary>
8085
public string UmbracoApplicationUrl { get; set; } = null!;
86+
87+
/// <summary>
88+
/// Gets or sets a value indicating whether strict domain matching is used when finding content to match the request.
89+
/// </summary>
90+
/// <remarks>
91+
/// <para>This setting is used within Umbraco's routing process based on content finders, specifically <see cref="Routing.ContentFinderByUrlNew" />.</para>
92+
/// <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>
93+
/// <para>If set to <see langword="true"/>, requests that don't match a configured domain will not be routed.</para>
94+
/// </remarks>
95+
[DefaultValue(StaticUseStrictDomainMatching)]
96+
public bool UseStrictDomainMatching { get; set; } = StaticUseStrictDomainMatching;
8197
}

src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
using Umbraco.Cms.Core.Configuration.Models;
35
using Umbraco.Cms.Core.DependencyInjection;
46
using Umbraco.Cms.Core.Models.PublishedContent;
57
using Umbraco.Cms.Core.PublishedCache;
@@ -19,25 +21,47 @@ public class ContentFinderByUrlNew : IContentFinder
1921
private readonly ILogger<ContentFinderByUrlNew> _logger;
2022
private readonly IPublishedContentCache _publishedContentCache;
2123
private readonly IDocumentUrlService _documentUrlService;
24+
private WebRoutingSettings _webRoutingSettings;
2225

2326
/// <summary>
2427
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
2528
/// </summary>
29+
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
2630
public ContentFinderByUrlNew(
2731
ILogger<ContentFinderByUrlNew> logger,
2832
IUmbracoContextAccessor umbracoContextAccessor,
2933
IDocumentUrlService documentUrlService,
3034
IPublishedContentCache publishedContentCache)
35+
: this(
36+
logger,
37+
umbracoContextAccessor,
38+
documentUrlService,
39+
publishedContentCache,
40+
StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<WebRoutingSettings>>())
3141
{
32-
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
42+
}
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="ContentFinderByUrl" /> class.
46+
/// </summary>
47+
public ContentFinderByUrlNew(
48+
ILogger<ContentFinderByUrlNew> logger,
49+
IUmbracoContextAccessor umbracoContextAccessor,
50+
IDocumentUrlService documentUrlService,
51+
IPublishedContentCache publishedContentCache,
52+
IOptionsMonitor<WebRoutingSettings> webRoutingSettings)
53+
{
54+
_logger = logger;
3355
_publishedContentCache = publishedContentCache;
3456
_documentUrlService = documentUrlService;
35-
UmbracoContextAccessor =
36-
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
57+
UmbracoContextAccessor = umbracoContextAccessor;
58+
59+
_webRoutingSettings = webRoutingSettings.CurrentValue;
60+
webRoutingSettings.OnChange(x => _webRoutingSettings = x);
3761
}
3862

3963
/// <summary>
40-
/// Gets the <see cref="IUmbracoContextAccessor" />
64+
/// Gets the <see cref="IUmbracoContextAccessor" />.
4165
/// </summary>
4266
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
4367

@@ -61,6 +85,14 @@ public virtual Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
6185
}
6286
else
6387
{
88+
// If we have configured strict domain matching, and a domain has not been found for the request configured on an ancestor node,
89+
// do not route the content by URL.
90+
if (_webRoutingSettings.UseStrictDomainMatching)
91+
{
92+
return Task.FromResult(false);
93+
}
94+
95+
// Default behaviour if strict domain matching is not enabled will be to route under the to the first root node found.
6496
route = frequest.AbsolutePathDecoded;
6597
}
6698

@@ -79,29 +111,24 @@ public virtual Task<bool> TryFindContent(IPublishedRequestBuilder frequest)
79111
return null;
80112
}
81113

82-
if (docreq == null)
83-
{
84-
throw new ArgumentNullException(nameof(docreq));
85-
}
114+
ArgumentNullException.ThrowIfNull(docreq);
86115

87116
if (_logger.IsEnabled(LogLevel.Debug))
88117
{
89118
_logger.LogDebug("Test route {Route}", route);
90119
}
91120

92-
var documentKey = _documentUrlService.GetDocumentKeyByRoute(
93-
docreq.Domain is null ? route : route.Substring(docreq.Domain.ContentId.ToString().Length),
121+
Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute(
122+
docreq.Domain is null ? route : route[docreq.Domain.ContentId.ToString().Length..],
94123
docreq.Culture,
95124
docreq.Domain?.ContentId,
96-
umbracoContext.InPreviewMode
97-
);
125+
umbracoContext.InPreviewMode);
98126

99127

100128
IPublishedContent? node = null;
101129
if (documentKey.HasValue)
102130
{
103131
node = _publishedContentCache.GetById(umbracoContext.InPreviewMode, documentKey.Value);
104-
//node = umbracoContext.Content?.GetById(umbracoContext.InPreviewMode, documentKey.Value);
105132
}
106133

107134
if (node != null)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using Microsoft.Extensions.Logging.Abstractions;
2+
using Microsoft.Extensions.Options;
3+
using Moq;
4+
using NUnit.Framework;
5+
using Umbraco.Cms.Core.Configuration.Models;
6+
using Umbraco.Cms.Core.Models.PublishedContent;
7+
using Umbraco.Cms.Core.PublishedCache;
8+
using Umbraco.Cms.Core.Routing;
9+
using Umbraco.Cms.Core.Services;
10+
using Umbraco.Cms.Core.Web;
11+
12+
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing;
13+
14+
[TestFixture]
15+
public class ContentFinderByUrlNewTests
16+
{
17+
private const int DomainContentId = 1233;
18+
private const int ContentId = 1234;
19+
private static readonly Guid _contentKey = Guid.NewGuid();
20+
private const string ContentPath = "/test-page";
21+
private const string DomainHost = "example.com";
22+
23+
[TestCase(ContentPath, true)]
24+
[TestCase("/missing-page", false)]
25+
public async Task Can_Find_Invariant_Content(string path, bool expectSuccess)
26+
{
27+
var mockContent = CreateMockPublishedContent();
28+
29+
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
30+
31+
var mockDocumentUrlService = CreateMockDocumentUrlService();
32+
33+
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
34+
35+
var sut = CreateContentFinder(mockUmbracoContextAccessor, mockDocumentUrlService, mockPublishedContentCache);
36+
37+
var publishedRequestBuilder = CreatePublishedRequestBuilder(path);
38+
39+
var result = await sut.TryFindContent(publishedRequestBuilder);
40+
41+
Assert.AreEqual(expectSuccess, result);
42+
if (expectSuccess)
43+
{
44+
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
45+
}
46+
else
47+
{
48+
Assert.IsNull(publishedRequestBuilder.PublishedContent);
49+
}
50+
}
51+
52+
[TestCase(ContentPath, true, false, true)]
53+
[TestCase("/missing-page", true, false, false)]
54+
[TestCase(ContentPath, true, true, true)]
55+
[TestCase(ContentPath, false, true, false)]
56+
public async Task Can_Find_Invariant_Content_With_Domain(string path, bool setDomain, bool useStrictDomainMatching, bool expectSuccess)
57+
{
58+
var mockContent = CreateMockPublishedContent();
59+
60+
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
61+
62+
var mockDocumentUrlService = CreateMockDocumentUrlService();
63+
64+
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
65+
66+
var sut = CreateContentFinder(
67+
mockUmbracoContextAccessor,
68+
mockDocumentUrlService,
69+
mockPublishedContentCache,
70+
new WebRoutingSettings
71+
{
72+
UseStrictDomainMatching = useStrictDomainMatching
73+
});
74+
75+
var publishedRequestBuilder = CreatePublishedRequestBuilder(path, withDomain: setDomain);
76+
77+
var result = await sut.TryFindContent(publishedRequestBuilder);
78+
79+
Assert.AreEqual(expectSuccess, result);
80+
if (expectSuccess)
81+
{
82+
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
83+
}
84+
else
85+
{
86+
Assert.IsNull(publishedRequestBuilder.PublishedContent);
87+
}
88+
}
89+
90+
private static Mock<IPublishedContent> CreateMockPublishedContent()
91+
{
92+
var mockContent = new Mock<IPublishedContent>();
93+
mockContent
94+
.SetupGet(x => x.Id)
95+
.Returns(ContentId);
96+
mockContent
97+
.SetupGet(x => x.ContentType.ItemType)
98+
.Returns(PublishedItemType.Content);
99+
return mockContent;
100+
}
101+
102+
private static Mock<IUmbracoContextAccessor> CreateMockUmbracoContextAccessor()
103+
{
104+
var mockUmbracoContext = new Mock<IUmbracoContext>();
105+
var mockUmbracoContextAccessor = new Mock<IUmbracoContextAccessor>();
106+
var umbracoContext = mockUmbracoContext.Object;
107+
mockUmbracoContextAccessor
108+
.Setup(x => x.TryGetUmbracoContext(out umbracoContext))
109+
.Returns(true);
110+
return mockUmbracoContextAccessor;
111+
}
112+
113+
private static Mock<IDocumentUrlService> CreateMockDocumentUrlService()
114+
{
115+
var mockDocumentUrlService = new Mock<IDocumentUrlService>();
116+
mockDocumentUrlService
117+
.Setup(x => x.GetDocumentKeyByRoute(It.Is<string>(y => y == ContentPath), It.IsAny<string?>(), It.IsAny<int?>(), It.IsAny<bool>()))
118+
.Returns(_contentKey);
119+
return mockDocumentUrlService;
120+
}
121+
122+
private static Mock<IPublishedContentCache> CreateMockPublishedContentCache(Mock<IPublishedContent> mockContent)
123+
{
124+
var mockPublishedContentCache = new Mock<IPublishedContentCache>();
125+
mockPublishedContentCache
126+
.Setup(x => x.GetById(It.IsAny<bool>(), It.Is<Guid>(y => y == _contentKey)))
127+
.Returns(mockContent.Object);
128+
return mockPublishedContentCache;
129+
}
130+
131+
private static ContentFinderByUrlNew CreateContentFinder(
132+
Mock<IUmbracoContextAccessor> mockUmbracoContextAccessor,
133+
Mock<IDocumentUrlService> mockDocumentUrlService,
134+
Mock<IPublishedContentCache> mockPublishedContentCache,
135+
WebRoutingSettings? webRoutingSettings = null)
136+
=> new(
137+
new NullLogger<ContentFinderByUrlNew>(),
138+
mockUmbracoContextAccessor.Object,
139+
mockDocumentUrlService.Object,
140+
mockPublishedContentCache.Object,
141+
Mock.Of<IOptionsMonitor<WebRoutingSettings>>(x => x.CurrentValue == (webRoutingSettings ?? new WebRoutingSettings())));
142+
143+
private static PublishedRequestBuilder CreatePublishedRequestBuilder(string path, bool withDomain = false)
144+
{
145+
var publishedRequestBuilder = new PublishedRequestBuilder(new Uri($"https://example.com{path}"), Mock.Of<IFileService>());
146+
if (withDomain)
147+
{
148+
publishedRequestBuilder.SetDomain(new DomainAndUri(new Domain(1, $"https://{DomainHost}/", DomainContentId, "en-US", false, 0), new Uri($"https://{DomainHost}{path}")));
149+
}
150+
151+
return publishedRequestBuilder;
152+
}
153+
}

0 commit comments

Comments
 (0)