Merge pull request #11130 from umbraco/v9/bugfix/dynamic-routing-fixes

UmbracoRouteValueTransformer fixes
This commit is contained in:
Bjarke Berg
2021-09-19 19:29:20 +02:00
committed by GitHub
4 changed files with 168 additions and 25 deletions

View File

@@ -14,6 +14,7 @@ using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
@@ -92,28 +93,28 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing
=> Mock.Of<IPublishedRouter>(x => x.RouteRequestAsync(It.IsAny<IPublishedRequestBuilder>(), It.IsAny<RouteRequestOptions>()) == Task.FromResult(request));
[Test]
public async Task Noop_When_Runtime_Level_Not_Run()
public async Task Null_When_Runtime_Level_Not_Run()
{
UmbracoRouteValueTransformer transformer = GetTransformer(
Mock.Of<IUmbracoContextAccessor>(),
Mock.Of<IRuntimeState>());
RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary());
Assert.AreEqual(0, result.Count);
Assert.IsNull(result);
}
[Test]
public async Task Noop_When_No_Umbraco_Context()
public async Task Null_When_No_Umbraco_Context()
{
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
Mock.Of<IUmbracoContextAccessor>());
RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary());
Assert.AreEqual(0, result.Count);
Assert.IsNull(result);
}
[Test]
public async Task Noop_When_Not_Document_Request()
public async Task Null_When_Not_Document_Request()
{
var umbracoContext = Mock.Of<IUmbracoContext>();
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
@@ -121,7 +122,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing
Mock.Of<IRoutableDocumentFilter>(x => x.IsDocumentRequest(It.IsAny<string>()) == false));
RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary());
Assert.AreEqual(0, result.Count);
Assert.IsNull(result);
}
[Test]
@@ -173,10 +174,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing
}
[Test]
public async Task Assigns_Values_To_RouteValueDictionary()
public async Task Assigns_Values_To_RouteValueDictionary_When_Content()
{
IUmbracoContext umbracoContext = GetUmbracoContext(true);
IPublishedRequest request = Mock.Of<IPublishedRequest>();
IPublishedRequest request = Mock.Of<IPublishedRequest>(x => x.PublishedContent == Mock.Of<IPublishedContent>());
UmbracoRouteValues routeValues = GetRouteValues(request);
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
@@ -190,6 +191,23 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing
Assert.AreEqual(routeValues.ActionName, result[ActionToken]);
}
[Test]
public async Task Returns_Null_RouteValueDictionary_When_No_Content()
{
IUmbracoContext umbracoContext = GetUmbracoContext(true);
IPublishedRequest request = Mock.Of<IPublishedRequest>(x => x.PublishedContent == null);
UmbracoRouteValues routeValues = GetRouteValues(request);
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
Mock.Of<IUmbracoContextAccessor>(x => x.TryGetUmbracoContext(out umbracoContext)),
router: GetRouter(request),
routeValuesFactory: GetRouteValuesFactory(request));
RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary());
Assert.IsNull(result);
}
private class TestController : RenderController
{
public TestController(ILogger<TestController> logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor)

View File

@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.DependencyInjection;
@@ -11,6 +13,7 @@ using Umbraco.Cms.Web.Website.Middleware;
using Umbraco.Cms.Web.Website.Models;
using Umbraco.Cms.Web.Website.Routing;
using Umbraco.Cms.Web.Website.ViewEngines;
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
namespace Umbraco.Extensions
{
@@ -38,8 +41,9 @@ namespace Umbraco.Extensions
builder.Services.AddDataProtection();
builder.Services.AddAntiforgery();
builder.Services.AddScoped<UmbracoRouteValueTransformer>();
builder.Services.AddSingleton<UmbracoRouteValueTransformer>();
builder.Services.AddSingleton<IControllerActionSearcher, ControllerActionSearcher>();
builder.Services.TryAddEnumerable(Singleton<MatcherPolicy, NotFoundSelectorPolicy>());
builder.Services.AddSingleton<IUmbracoRouteValuesFactory, UmbracoRouteValuesFactory>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Routing;
namespace Umbraco.Cms.Web.Website.Routing
{
/// <summary>
/// Used to handle 404 routes that haven't been handled by the end user
/// </summary>
internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
private readonly Lazy<Endpoint> _notFound;
private readonly EndpointDataSource _endpointDataSource;
public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource)
{
_notFound = new Lazy<Endpoint>(GetNotFoundEndpoint);
_endpointDataSource = endpointDataSource;
}
// return the endpoint for the RenderController.Index action.
private Endpoint GetNotFoundEndpoint()
{
Endpoint e = _endpointDataSource.Endpoints.First(x =>
{
// return the endpoint for the RenderController.Index action.
ControllerActionDescriptor descriptor = x.Metadata?.GetMetadata<ControllerActionDescriptor>();
return descriptor?.ControllerTypeInfo == typeof(RenderController)
&& descriptor?.ActionName == nameof(RenderController.Index);
});
return e;
}
public override int Order => 0;
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
// Don't apply this filter to any endpoint group that is a controller route
// i.e. only dynamic routes.
foreach (Endpoint endpoint in endpoints)
{
ControllerAttribute controller = endpoint.Metadata?.GetMetadata<ControllerAttribute>();
if (controller != null)
{
return false;
}
}
// then ensure this is only applied if all endpoints are IDynamicEndpointMetadata
return endpoints.All(x => x.Metadata?.GetMetadata<IDynamicEndpointMetadata>() != null);
}
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
if (AllInvalid(candidates))
{
UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues?.PublishedRequest != null
&& !umbracoRouteValues.PublishedRequest.HasPublishedContent()
&& umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound)
{
// not found/404
httpContext.SetEndpoint(_notFound.Value);
}
}
return Task.CompletedTask;
}
private static bool AllInvalid(CandidateSet candidates)
{
for (int i = 0; i < candidates.Count; i++)
{
if (candidates.IsValidCandidate(i))
{
return false;
}
}
return true;
}
}
}

View File

@@ -28,6 +28,7 @@ using RouteDirection = Umbraco.Cms.Core.Routing.RouteDirection;
namespace Umbraco.Cms.Web.Website.Routing
{
/// <summary>
/// The route value transformer for Umbraco front-end routes
/// </summary>
@@ -92,31 +93,41 @@ namespace Umbraco.Cms.Web.Website.Routing
// If we aren't running, then we have nothing to route
if (_runtime.Level != RuntimeLevel.Run)
{
return values;
return null;
}
// will be null for any client side requests like JS, etc...
if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext umbracoContext))
{
return values;
return null;
}
if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path))
{
return values;
return null;
}
// Don't execute if there are already UmbracoRouteValues assigned.
// This can occur if someone else is dynamically routing and in which case we don't want to overwrite
// the routing work being done there.
UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues != null)
{
return null;
}
// Check if there is no existing content and return the no content controller
if (!umbracoContext.Content.HasContent())
{
values[ControllerToken] = ControllerExtensions.GetControllerName<RenderNoContentController>();
values[ActionToken] = nameof(RenderNoContentController.Index);
return values;
return new RouteValueDictionary
{
[ControllerToken] = ControllerExtensions.GetControllerName<RenderNoContentController>(),
[ActionToken] = nameof(RenderNoContentController.Index)
};
}
IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, umbracoContext);
UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
// Store the route values as a httpcontext feature
httpContext.Features.Set(umbracoRouteValues);
@@ -125,16 +136,32 @@ namespace Umbraco.Cms.Web.Website.Routing
PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values);
if (postedInfo != null)
{
return HandlePostedValues(postedInfo, httpContext, values);
return HandlePostedValues(postedInfo, httpContext);
}
values[ControllerToken] = umbracoRouteValues.ControllerName;
if (!umbracoRouteValues?.PublishedRequest?.HasPublishedContent() ?? false)
{
// No content was found, not by any registered 404 handlers and
// not by the IContentLastChanceFinder. In this case we want to return
// our default 404 page but we cannot return route values now because
// it's possible that a developer is handling dynamic routes too.
// Our 404 page will be handled with the NotFoundSelectorPolicy
return null;
}
// See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_
// We should apparenlty not be modified these values.
// So we create new ones.
var newValues = new RouteValueDictionary
{
[ControllerToken] = umbracoRouteValues.ControllerName
};
if (string.IsNullOrWhiteSpace(umbracoRouteValues.ActionName) == false)
{
values[ActionToken] = umbracoRouteValues.ActionName;
newValues[ActionToken] = umbracoRouteValues.ActionName;
}
return values;
return newValues;
}
private async Task<IPublishedRequest> RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext)
@@ -196,11 +223,14 @@ namespace Umbraco.Cms.Web.Website.Routing
};
}
private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext, RouteValueDictionary values)
private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext)
{
// set the standard route values/tokens
values[ControllerToken] = postedInfo.ControllerName;
values[ActionToken] = postedInfo.ActionName;
var values = new RouteValueDictionary
{
[ControllerToken] = postedInfo.ControllerName,
[ActionToken] = postedInfo.ActionName
};
ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find<SurfaceController>(httpContext, postedInfo.ControllerName, postedInfo.ActionName);