Skip to content

Commit e7ccfaa

Browse files
authored
Routing: Added method to IDocumentUrlService for retrieving document key from URI (closes #20666) (#20673)
Added method to IDocumentUrlService for retrieving document key from URI.
1 parent 8733230 commit e7ccfaa

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

src/Umbraco.Core/Routing/DomainUtilities.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Globalization;
3-
using Microsoft.Extensions.DependencyInjection;
4-
using Umbraco.Cms.Core.DependencyInjection;
53
using Umbraco.Cms.Core.Models.PublishedContent;
64
using Umbraco.Cms.Core.PublishedCache;
75
using Umbraco.Cms.Core.Services.Navigation;

src/Umbraco.Core/Services/DocumentUrlService.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
using System.Diagnostics.CodeAnalysis;
33
using System.Globalization;
44
using System.Runtime.CompilerServices;
5+
using Microsoft.Extensions.DependencyInjection;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78
using Umbraco.Cms.Core.Configuration.Models;
9+
using Umbraco.Cms.Core.DependencyInjection;
810
using Umbraco.Cms.Core.Models;
911
using Umbraco.Cms.Core.Persistence.Repositories;
1012
using Umbraco.Cms.Core.PublishedCache;
@@ -28,6 +30,7 @@ public class DocumentUrlService : IDocumentUrlService
2830
private readonly IDocumentRepository _documentRepository;
2931
private readonly ICoreScopeProvider _coreScopeProvider;
3032
private readonly GlobalSettings _globalSettings;
33+
private readonly WebRoutingSettings _webRoutingSettings;
3134
private readonly UrlSegmentProviderCollection _urlSegmentProviderCollection;
3235
private readonly IContentService _contentService;
3336
private readonly IShortStringHelper _shortStringHelper;
@@ -37,6 +40,7 @@ public class DocumentUrlService : IDocumentUrlService
3740
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
3841
private readonly IPublishStatusQueryService _publishStatusQueryService;
3942
private readonly IDomainCacheService _domainCacheService;
43+
private readonly IDefaultCultureAccessor _defaultCultureAccessor;
4044

4145
private readonly ConcurrentDictionary<string, PublishedDocumentUrlSegments> _cache = new();
4246
private bool _isInitialized;
@@ -96,6 +100,7 @@ public UrlSegment(string segment, bool isPrimary)
96100
/// <summary>
97101
/// Initializes a new instance of the <see cref="DocumentUrlService"/> class.
98102
/// </summary>
103+
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
99104
public DocumentUrlService(
100105
ILogger<DocumentUrlService> logger,
101106
IDocumentUrlRepository documentUrlRepository,
@@ -111,12 +116,53 @@ public DocumentUrlService(
111116
IDocumentNavigationQueryService documentNavigationQueryService,
112117
IPublishStatusQueryService publishStatusQueryService,
113118
IDomainCacheService domainCacheService)
119+
:this(
120+
logger,
121+
documentUrlRepository,
122+
documentRepository,
123+
coreScopeProvider,
124+
globalSettings,
125+
StaticServiceProvider.Instance.GetRequiredService<IOptions<WebRoutingSettings>>(),
126+
urlSegmentProviderCollection,
127+
contentService,
128+
shortStringHelper,
129+
languageService,
130+
keyValueService,
131+
idKeyMap,
132+
documentNavigationQueryService,
133+
publishStatusQueryService,
134+
domainCacheService,
135+
StaticServiceProvider.Instance.GetRequiredService<IDefaultCultureAccessor>())
136+
{
137+
}
138+
139+
/// <summary>
140+
/// Initializes a new instance of the <see cref="DocumentUrlService"/> class.
141+
/// </summary>
142+
public DocumentUrlService(
143+
ILogger<DocumentUrlService> logger,
144+
IDocumentUrlRepository documentUrlRepository,
145+
IDocumentRepository documentRepository,
146+
ICoreScopeProvider coreScopeProvider,
147+
IOptions<GlobalSettings> globalSettings,
148+
IOptions<WebRoutingSettings> webRoutingSettings,
149+
UrlSegmentProviderCollection urlSegmentProviderCollection,
150+
IContentService contentService,
151+
IShortStringHelper shortStringHelper,
152+
ILanguageService languageService,
153+
IKeyValueService keyValueService,
154+
IIdKeyMap idKeyMap,
155+
IDocumentNavigationQueryService documentNavigationQueryService,
156+
IPublishStatusQueryService publishStatusQueryService,
157+
IDomainCacheService domainCacheService,
158+
IDefaultCultureAccessor defaultCultureAccessor)
114159
{
115160
_logger = logger;
116161
_documentUrlRepository = documentUrlRepository;
117162
_documentRepository = documentRepository;
118163
_coreScopeProvider = coreScopeProvider;
119164
_globalSettings = globalSettings.Value;
165+
_webRoutingSettings = webRoutingSettings.Value;
120166
_urlSegmentProviderCollection = urlSegmentProviderCollection;
121167
_contentService = contentService;
122168
_shortStringHelper = shortStringHelper;
@@ -126,6 +172,7 @@ public DocumentUrlService(
126172
_documentNavigationQueryService = documentNavigationQueryService;
127173
_publishStatusQueryService = publishStatusQueryService;
128174
_domainCacheService = domainCacheService;
175+
_defaultCultureAccessor = defaultCultureAccessor;
129176
}
130177

131178
/// <inheritdoc/>
@@ -494,6 +541,37 @@ public async Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeysEnumera
494541
scope.Complete();
495542
}
496543

544+
/// <inheritdoc/>
545+
public Guid? GetDocumentKeyByUri(Uri uri, bool isDraft)
546+
{
547+
IEnumerable<Domain> domains = _domainCacheService.GetAll(false);
548+
DomainAndUri? domain = DomainUtilities.SelectDomain(domains, uri, defaultCulture: _defaultCultureAccessor.DefaultCulture);
549+
550+
string route;
551+
if (domain is not null)
552+
{
553+
route = domain.ContentId + DomainUtilities.PathRelativeToDomain(domain.Uri, uri.GetAbsolutePathDecoded());
554+
}
555+
else
556+
{
557+
// If we have configured strict domain matching, and a domain has not been found for the request configured on an ancestor node,
558+
// do not route the content by URL.
559+
if (_webRoutingSettings.UseStrictDomainMatching)
560+
{
561+
return null;
562+
}
563+
564+
// Default behaviour if strict domain matching is not enabled will be to route under the to the first root node found.
565+
route = uri.GetAbsolutePathDecoded();
566+
}
567+
568+
return GetDocumentKeyByRoute(
569+
domain is null ? route : route[domain.ContentId.ToString().Length..],
570+
domain?.Culture,
571+
domain?.ContentId,
572+
isDraft);
573+
}
574+
497575
/// <inheritdoc/>
498576
public Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft)
499577
{

src/Umbraco.Core/Services/IDocumentUrlService.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Umbraco.Cms.Core.Models;
2-
using Umbraco.Cms.Core.Routing;
32

43
namespace Umbraco.Cms.Core.Services;
54

@@ -64,6 +63,14 @@ IEnumerable<string> GetUrlSegments(Guid documentKey, string culture, bool isDraf
6463
/// <param name="documentKeys">The collection of document keys.</param>
6564
Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeys);
6665

66+
/// <summary>
67+
/// Gets a document key by <see cref="Uri" />.
68+
/// </summary>
69+
/// <param name="uri">The uniform resource identifier.</param>
70+
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
71+
/// <returns>The document key, or null if not found.</returns>
72+
Guid? GetDocumentKeyByUri(Uri uri, bool isDraft) => throw new NotImplementedException(); // TODO (V19): Remove default implementation.
73+
6774
/// <summary>
6875
/// Gets a document key by route.
6976
/// </summary>

tests/Umbraco.Tests.Common/Builders/TemplateBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ public override ITemplate Build()
129129
return template;
130130
}
131131

132-
public static Template CreateTextPageTemplate(string alias = "textPage") =>
132+
public static Template CreateTextPageTemplate(string alias = "textPage", string name = "Text page") =>
133133
(Template)new TemplateBuilder()
134134
.WithAlias(alias)
135-
.WithName("Text page")
135+
.WithName(name)
136136
.Build();
137137
}

tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using NUnit.Framework;
22
using Umbraco.Cms.Core.Cache;
33
using Umbraco.Cms.Core.Models;
4+
using Umbraco.Cms.Core.Models.ContentEditing;
45
using Umbraco.Cms.Core.Notifications;
56
using Umbraco.Cms.Core.Services;
67
using Umbraco.Cms.Core.Strings;
78
using Umbraco.Cms.Core.Sync;
89
using Umbraco.Cms.Tests.Common.Builders;
10+
using Umbraco.Cms.Tests.Common.Builders.Extensions;
911
using Umbraco.Cms.Tests.Common.Testing;
1012
using Umbraco.Cms.Tests.Integration.Testing;
1113
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
@@ -23,6 +25,8 @@ internal sealed class DocumentUrlServiceTests : UmbracoIntegrationTestWithConten
2325

2426
protected ILanguageService LanguageService => GetRequiredService<ILanguageService>();
2527

28+
protected IDomainService DomainService => GetRequiredService<IDomainService>();
29+
2630
protected override void CustomTestSetup(IUmbracoBuilder builder)
2731
{
2832
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
@@ -148,6 +152,64 @@ public async Task GetUrlSegment_For_Published_Then_Deleted_Document_Does_Not_Hav
148152
Assert.IsNull(actual);
149153
}
150154

155+
[TestCase("/", ExpectedResult = TextpageKey)]
156+
[TestCase("/text-page-1", ExpectedResult = SubPageKey)]
157+
public string? GetDocumentKeyByUri_Without_Domains_Returns_Expected_DocumentKey(string path)
158+
{
159+
ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
160+
161+
var uri = new Uri("http://example.com" + path);
162+
return DocumentUrlService.GetDocumentKeyByUri(uri, false)?.ToString()?.ToUpper();
163+
}
164+
165+
private const string VariantRootPageKey = "1D3283C7-64FD-4F4D-A741-442BDA487B71";
166+
private const string VariantChildPageKey = "1D3283C7-64FD-4F4D-A741-442BDA487B72";
167+
168+
[TestCase("/", "/en", "http://example.com/en/", ExpectedResult = VariantRootPageKey)]
169+
[TestCase("/child-page", "/en", "http://example.com/en/", ExpectedResult = VariantChildPageKey)]
170+
[TestCase("/", "example.com", "http://example.com/", ExpectedResult = VariantRootPageKey)]
171+
[TestCase("/child-page", "example.com", "http://example.com/", ExpectedResult = VariantChildPageKey)]
172+
public async Task<string?> GetDocumentKeyByUri_With_Domains_Returns_Expected_DocumentKey(string path, string domain, string rootUrl)
173+
{
174+
var template = TemplateBuilder.CreateTextPageTemplate("variantPageTemplate", "Variant Page Template");
175+
FileService.SaveTemplate(template);
176+
177+
var contentType = new ContentTypeBuilder()
178+
.WithAlias("variantPage")
179+
.WithName("Variant Page")
180+
.WithContentVariation(ContentVariation.Culture)
181+
.WithAllowAsRoot(true)
182+
.WithDefaultTemplateId(template.Id)
183+
.Build();
184+
ContentTypeService.Save(contentType);
185+
186+
var rootPage = new ContentBuilder()
187+
.WithKey(Guid.Parse(VariantRootPageKey))
188+
.WithContentType(contentType)
189+
.WithCultureName("en-US", $"Root Page")
190+
.Build();
191+
var childPage = new ContentBuilder()
192+
.WithKey(Guid.Parse(VariantChildPageKey))
193+
.WithContentType(contentType)
194+
.WithCultureName("en-US", $"Child Page")
195+
.WithParent(rootPage)
196+
.Build();
197+
ContentService.Save(rootPage, -1);
198+
ContentService.Save(childPage, -1);
199+
ContentService.PublishBranch(rootPage, PublishBranchFilter.IncludeUnpublished, ["*"]);
200+
201+
var updateDomainResult = await DomainService.UpdateDomainsAsync(
202+
rootPage.Key,
203+
new DomainsUpdateModel
204+
{
205+
Domains = [new DomainModel { DomainName = domain, IsoCode = "en-US" }],
206+
});
207+
Assert.IsTrue(updateDomainResult.Success);
208+
209+
var uri = new Uri(rootUrl + path);
210+
return DocumentUrlService.GetDocumentKeyByUri(uri, false)?.ToString()?.ToUpper();
211+
}
212+
151213
[TestCase("/", "en-US", true, ExpectedResult = TextpageKey)]
152214
[TestCase("/text-page-1", "en-US", true, ExpectedResult = SubPageKey)]
153215
[TestCase("/text-page-1-custom", "en-US", true, ExpectedResult = SubPageKey)] // Uses the segment registered by the custom IIUrlSegmentProvider that allows for more than one segment per document.
@@ -160,7 +222,7 @@ public async Task GetUrlSegment_For_Published_Then_Deleted_Document_Does_Not_Hav
160222
[TestCase("/text-page-2", "en-US", false, ExpectedResult = null)]
161223
[TestCase("/text-page-2-custom", "en-US", false, ExpectedResult = SubPage2Key)] // Uses the segment registered by the custom IIUrlSegmentProvider that does not allow for more than one segment per document.
162224
[TestCase("/text-page-3", "en-US", false, ExpectedResult = SubPage3Key)]
163-
public string? GetDocumentKeyByRoute_Returns_Expected_Route(string route, string isoCode, bool loadDraft)
225+
public string? GetDocumentKeyByRoute_Returns_Expected_DocumentKey(string route, string isoCode, bool loadDraft)
164226
{
165227
if (loadDraft is false)
166228
{

0 commit comments

Comments
 (0)