diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs index 4fed35a28196..f60fa442651b 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs @@ -145,6 +145,11 @@ private async Task HandleRequest(string path) path = DecodePath(path); path = path.Length == 0 ? "/" : path; + if (_apiContentPathResolver.IsResolvablePath(path) is false) + { + return NotFound(); + } + IPublishedContent? contentItem = GetContent(path); if (contentItem is not null) { diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs b/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs index 4ca6be3932aa..cb3613b4e042 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentPathResolver.cs @@ -15,6 +15,23 @@ public ApiContentPathResolver(IRequestRoutingService requestRoutingService, IApi _apiPublishedContentCache = apiPublishedContentCache; } + public virtual bool IsResolvablePath(string path) + { + // File requests will blow up with an downstream exception in GetRequiredPublishedSnapshot, which fails due to an UmbracoContext + // not being available for what's considered a static file request. + // See: https://github.com/umbraco/Umbraco-CMS/issues/19051 + // Given a URL segment and hence route can't contain a period, we can safely assume that if the last segment of the path contains + // a period, it's a file request and should return null here. + if (IsFileRequest(path)) + { + return false; + } + + return true; + } + + private static bool IsFileRequest(string path) => path.Split('/', StringSplitOptions.RemoveEmptyEntries).Last().Contains('.'); + public virtual IPublishedContent? ResolveContentPath(string path) { path = path.EnsureStartsWith("/"); diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs b/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs index 391f18998e58..2e7a92c7a3cc 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiContentPathResolver.cs @@ -4,5 +4,7 @@ namespace Umbraco.Cms.Core.DeliveryApi; public interface IApiContentPathResolver { + bool IsResolvablePath(string path) => true; + IPublishedContent? ResolveContentPath(string path); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiContentPathResolverTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiContentPathResolverTests.cs new file mode 100644 index 000000000000..7e4f093794c6 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiContentPathResolverTests.cs @@ -0,0 +1,47 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; + +[TestFixture] +public class ApiContentPathResolverTests +{ + private const string TestPath = "/test/page"; + + [TestCase(TestPath, true)] + [TestCase("file.txt", false)] + [TestCase("test/file.txt", false)] + [TestCase("test/test2/file.txt", false)] + [TestCase("/file.txt", false)] + [TestCase("/test/file.txt", false)] + [TestCase("/test/test2/file.txt", false)] + public void Can_Verify_Resolveable_Paths(string path, bool expected) + { + var resolver = CreateResolver(); + var result = resolver.IsResolvablePath(path); + Assert.AreEqual(expected, result); + } + + [Test] + public void Resolves_Content_For_Path() + { + var resolver = CreateResolver(); + var result = resolver.ResolveContentPath(TestPath); + Assert.IsNotNull(result); + } + + private static ApiContentPathResolver CreateResolver() + { + var mockRequestRoutingService = new Mock(); + mockRequestRoutingService + .Setup(x => x.GetContentRoute(It.IsAny())) + .Returns((string path) => path); + var mockApiPublishedContentCache = new Mock(); + mockApiPublishedContentCache + .Setup(x => x.GetByRoute(It.Is(y => y == TestPath))) + .Returns(new Mock().Object); + return new ApiContentPathResolver(mockRequestRoutingService.Object, mockApiPublishedContentCache.Object); + } +}