diff --git a/src/Umbraco.Cms.Api.Management/Factories/RedirectUrlPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/RedirectUrlPresentationFactory.cs index 8a62d299eb..8cb09bf03a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RedirectUrlPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RedirectUrlPresentationFactory.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.RedirectUrlManagement; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; @@ -22,6 +22,12 @@ public class RedirectUrlPresentationFactory : IRedirectUrlPresentationFactory var originalUrl = _publishedUrlProvider.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); + // Even if the URL could not be extracted from the route, if we have a path as a the route for the original URL, we should display it. + if (originalUrl == "#" && source.Url.StartsWith('/')) + { + originalUrl = source.Url; + } + return new RedirectUrlResponseModel { OriginalUrl = originalUrl, diff --git a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs index c6f8a49fcd..adc367a97f 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs @@ -53,21 +53,38 @@ public class ContentFinderByRedirectUrl : IContentFinder return false; } - var route = frequest.Domain != null - ? frequest.Domain.ContentId + - DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) - : frequest.AbsolutePathDecoded; - + var route = frequest.AbsolutePathDecoded; IRedirectUrl? redirectUrl = await _redirectUrlService.GetMostRecentRedirectUrlAsync(route, frequest.Culture); - if (redirectUrl == null) + if (redirectUrl is null) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("No match for route: {Route}", route); } - return false; + // Routes under domains can be stored with the integer ID of the content where the domains were defined as the first part of the route, + // so if we haven't found a redirect, try using that format too. + // See: https://github.com/umbraco/Umbraco-CMS/pull/18160 and https://github.com/umbraco/Umbraco-CMS/pull/18763 + if (frequest.Domain is not null) + { + route = frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); + redirectUrl = await _redirectUrlService.GetMostRecentRedirectUrlAsync(route, frequest.Culture); + + if (redirectUrl is null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("No match for route with domain: {Route}", route); + } + + return false; + } + } + else + { + return false; + } } IPublishedContent? content = umbracoContext.Content?.GetById(redirectUrl.ContentId); diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs index 314f2c5598..c3e779b0d7 100644 --- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -249,7 +249,8 @@ public class NewDefaultUrlProvider : IUrlProvider culture); var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || string.IsNullOrEmpty(culture) || + if (domainUri is not null || + string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { var url = AssembleUrl(domainUri, path, current, mode).ToString(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByRedirectUrlTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByRedirectUrlTests.cs new file mode 100644 index 0000000000..025a1ef655 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByRedirectUrlTests.cs @@ -0,0 +1,162 @@ +using System.Net; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +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 ContentFinderByRedirectUrlTests +{ + private const int DomainContentId = 1233; + private const int ContentId = 1234; + + [Test] + public async Task Can_Find_Invariant_Content() + { + const string OldPath = "/old-page-path"; + const string NewPath = "/new-page-path"; + + var mockRedirectUrlService = CreateMockRedirectUrlService(OldPath); + + var mockContent = CreateMockPublishedContent(); + + var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor(mockContent); + + var mockPublishedUrlProvider = CreateMockPublishedUrlProvider(NewPath); + + var sut = CreateContentFinder(mockRedirectUrlService, mockUmbracoContextAccessor, mockPublishedUrlProvider); + + var publishedRequestBuilder = CreatePublishedRequestBuilder(OldPath); + + var result = await sut.TryFindContent(publishedRequestBuilder); + + AssertRedirectResult(publishedRequestBuilder, result); + } + + [Test] + public async Task Can_Find_Variant_Content_With_Path_Root() + { + const string OldPath = "/en/old-page-path"; + const string NewPath = "/en/new-page-path"; + + var mockRedirectUrlService = CreateMockRedirectUrlService(OldPath); + + var mockContent = CreateMockPublishedContent(); + + var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor(mockContent); + + var mockPublishedUrlProvider = CreateMockPublishedUrlProvider(NewPath); + + var sut = CreateContentFinder(mockRedirectUrlService, mockUmbracoContextAccessor, mockPublishedUrlProvider); + + var publishedRequestBuilder = CreatePublishedRequestBuilder(OldPath, withDomain: true); + + var result = await sut.TryFindContent(publishedRequestBuilder); + + AssertRedirectResult(publishedRequestBuilder, result); + } + + [Test] + public async Task Can_Find_Variant_Content_With_Domain_Node_Id_Prefixed_Path() + { + const string OldPath = "/en/old-page-path"; + var domainPrefixedOldPath = $"{DomainContentId}/old-page-path"; + const string NewPath = "/en/new-page-path"; + + var mockRedirectUrlService = CreateMockRedirectUrlService(domainPrefixedOldPath); + + var mockContent = CreateMockPublishedContent(); + + var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor(mockContent); + + var mockPublishedUrlProvider = CreateMockPublishedUrlProvider(NewPath); + + var sut = CreateContentFinder(mockRedirectUrlService, mockUmbracoContextAccessor, mockPublishedUrlProvider); + + var publishedRequestBuilder = CreatePublishedRequestBuilder(OldPath, withDomain: true); + + var result = await sut.TryFindContent(publishedRequestBuilder); + + AssertRedirectResult(publishedRequestBuilder, result); + } + + private static Mock CreateMockRedirectUrlService(string oldPath) + { + var mockRedirectUrlService = new Mock(); + mockRedirectUrlService + .Setup(x => x.GetMostRecentRedirectUrlAsync(It.Is(y => y == oldPath), It.IsAny())) + .ReturnsAsync(new RedirectUrl + { + ContentId = ContentId, + }); + return mockRedirectUrlService; + } + + private static Mock CreateMockPublishedUrlProvider(string newPath) + { + var mockPublishedUrlProvider = new Mock(); + mockPublishedUrlProvider + .Setup(x => x.GetUrl(It.Is(y => y.Id == ContentId), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(newPath); + return mockPublishedUrlProvider; + } + + 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(Mock mockContent) + { + var mockUmbracoContext = new Mock(); + mockUmbracoContext + .Setup(x => x.Content.GetById(It.Is(y => y == ContentId))) + .Returns(mockContent.Object); + var mockUmbracoContextAccessor = new Mock(); + var umbracoContext = mockUmbracoContext.Object; + mockUmbracoContextAccessor + .Setup(x => x.TryGetUmbracoContext(out umbracoContext)) + .Returns(true); + return mockUmbracoContextAccessor; + } + + private static ContentFinderByRedirectUrl CreateContentFinder( + Mock mockRedirectUrlService, + Mock mockUmbracoContextAccessor, + Mock mockPublishedUrlProvider) + => new ContentFinderByRedirectUrl( + mockRedirectUrlService.Object, + new NullLogger(), + mockPublishedUrlProvider.Object, + mockUmbracoContextAccessor.Object); + + 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, "/en", DomainContentId, "en-US", false, 0), new Uri($"https://example.com{path}"))); + } + + return publishedRequestBuilder; + } + + private static void AssertRedirectResult(PublishedRequestBuilder publishedRequestBuilder, bool result) + { + Assert.AreEqual(true, result); + Assert.AreEqual(HttpStatusCode.Moved, (HttpStatusCode)publishedRequestBuilder.ResponseStatusCode); + } +}