Fix for issue 13017 - BeginUmbracoForm doesn't work with custom umbraco routes (#13103)

* Fix issue with custom Umbraco routes not working after submitting to a Surface controller

* Added comments

* Fixed breaking changes

* Fixed test by using correct new ctor

* Fixed initializtion of UmbracoRouteValueTransformer due to ambiguous ctor

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Justin Neville
2022-12-06 11:39:43 +00:00
committed by GitHub
parent 5e03236e70
commit d08f21d638
9 changed files with 432 additions and 50 deletions

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Umbraco.Cms.Web.Common.Extensions;
/// <summary>
/// Extensions methods for EndpointDataSource.
/// </summary>
public static class EndpointDataSourceExtensions
{
/// <summary>
/// Gets an endpoint that matches the specified path.
/// </summary>
/// <param name="endpointDatasource">The end point data source.</param>
/// <param name="linkParser">The link parsers using to parse the path.</param>
/// <param name="path">The path to be parsed.</param>
/// <param name="routeValues">The mathcing route values for the mathcing endpoint.</param>
/// <returns>The endpoint or null if not found.</returns>
public static Endpoint? GetEndpointByPath(this EndpointDataSource endpointDatasource, LinkParser linkParser, PathString path, out RouteValueDictionary? routeValues)
{
routeValues = null;
foreach (Endpoint endpoint in endpointDatasource.Endpoints)
{
routeValues = linkParser.ParsePathByEndpoint(endpoint, path);
if (routeValues != null)
{
return endpoint;
}
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
namespace Umbraco.Cms.Web.Common.Extensions;
/// <summary>
/// Extensions methods for Endpoint.
/// </summary>
public static class EndpointExtensions
{
/// <summary>
/// Gets the controller action descriptor from the endpoint.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <returns>The controller action descriptor or null if not found.</returns>
public static ControllerActionDescriptor? GetControllerActionDescriptor(this Endpoint endpoint)
{
return endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
}
/// <summary>
/// Gets the route name metadata from the endpoint.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <returns>The route name metadata or null if not found.</returns>
public static RouteNameMetadata? GetRouteNameMetadata(this Endpoint endpoint)
{
return endpoint.Metadata.GetMetadata<RouteNameMetadata>();
}
/// <summary>
/// Gets the route name from the endpoint metadata.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <returns>The route name or null if not found.</returns>
public static string? GetRouteName(this Endpoint endpoint)
{
return endpoint.GetRouteNameMetadata()?.RouteName;
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Umbraco.Cms.Web.Common.Extensions;
/// <summary>
/// Extension methods for LinkParser.
/// </summary>
public static class LinkParserExtensions
{
/// <summary>
/// Parses the path using the specified endpoint and returns the route values if a the path matches the route pattern.
/// </summary>
/// <param name="linkParser">The link parser.</param>
/// <param name="endpoint">The endpoint.</param>
/// <param name="path">The path to parse.</param>
/// <returns>The route values if the path matches or null.</returns>
public static RouteValueDictionary? ParsePathByEndpoint(this LinkParser linkParser, Endpoint endpoint, PathString path)
{
var name = endpoint.GetRouteName();
if (name != null)
{
return linkParser.ParsePathByEndpointName(name, path);
}
else
{
return null;
}
}
}

View File

@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Routing;
@@ -21,23 +19,42 @@ public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter
/// <inheritdoc />
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Endpoint? endpoint = context.HttpContext.GetEndpoint();
// Check if there's proxied ViewData (i.e. returned from a SurfaceController)
// We don't want to find the content and set route values again if this is from a surface controller
ProxyViewDataFeature? proxyViewDataFeature = context.HttpContext.Features.Get<ProxyViewDataFeature>();
// Check if there is any delegate in the metadata of the route, this
// will occur when using the ForUmbraco method during routing.
CustomRouteContentFinderDelegate? contentFinder =
endpoint?.Metadata.OfType<CustomRouteContentFinderDelegate>().FirstOrDefault();
if (contentFinder != null)
if (proxyViewDataFeature != null)
{
await SetUmbracoRouteValues(context, contentFinder.FindContent(context));
if (context.Controller is Controller controller)
{
foreach (KeyValuePair<string, object?> kv in proxyViewDataFeature.ViewData)
{
controller.ViewData[kv.Key] = kv.Value;
}
}
}
else
{
// Check if the controller is IVirtualPageController and then use that to FindContent
if (context.Controller is IVirtualPageController ctrl)
Endpoint? endpoint = context.HttpContext.GetEndpoint();
if (endpoint != null)
{
await SetUmbracoRouteValues(context, ctrl.FindContent(context));
IUmbracoVirtualPageRoute umbracoVirtualPageRoute = context.HttpContext.RequestServices.GetRequiredService<IUmbracoVirtualPageRoute>();
IPublishedContent? publishedContent = umbracoVirtualPageRoute.FindContent(endpoint, context);
if (publishedContent != null)
{
await umbracoVirtualPageRoute.SetRouteValues(
context.HttpContext,
publishedContent,
(ControllerActionDescriptor)context.ActionDescriptor);
}
else
{
// if there is no content then it should be a not found
context.Result = new NotFoundResult();
}
}
}
@@ -47,32 +64,4 @@ public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter
await next();
}
}
private async Task SetUmbracoRouteValues(ActionExecutingContext context, IPublishedContent? content)
{
if (content != null)
{
UriUtility uriUtility = context.HttpContext.RequestServices.GetRequiredService<UriUtility>();
var originalRequestUrl = new Uri(context.HttpContext.Request.GetEncodedUrl());
Uri cleanedUrl = uriUtility.UriToUmbraco(originalRequestUrl);
IPublishedRouter router = context.HttpContext.RequestServices.GetRequiredService<IPublishedRouter>();
IPublishedRequestBuilder requestBuilder = await router.CreateRequestAsync(cleanedUrl);
requestBuilder.SetPublishedContent(content);
IPublishedRequest publishedRequest = requestBuilder.Build();
var routeValues = new UmbracoRouteValues(
publishedRequest,
(ControllerActionDescriptor)context.ActionDescriptor);
context.HttpContext.Features.Set(routeValues);
}
else
{
// if there is no content then it should be a not found
context.Result = new NotFoundResult();
}
}
}

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
namespace Umbraco.Cms.Web.Common.Routing;
/// <summary>
/// This is used to setup the virtual page route so the route values and content are set for virtual pages.
/// </summary>
public interface IUmbracoVirtualPageRoute
{
/// <summary>
/// This sets up the virtual page route for the current request if a mtahcing endpoint is found.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>Nothing</returns>
Task SetupVirtualPageRoute(HttpContext httpContext);
/// <summary>
/// Finds the content from the custom route finder delegate or the virtual page controller.
/// Note - This creates a dummay action executing context so the FindContent method of the
/// IVirtualPageController can be called (without changing the interface contract).
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="routeValues">The route values.</param>
/// <param name="controllerActionDescriptor">The action descriptor.</param>
/// <param name="controller">The controller.</param>
/// <returns></returns>
IPublishedContent? FindContent(
Endpoint endpoint,
HttpContext httpContext,
RouteValueDictionary routeValues,
ControllerActionDescriptor controllerActionDescriptor,
object controller);
/// <summary>
/// Finds the content from the custom route finder delegate or the virtual page controller.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="actionExecutingContext">The action executing context.</param>
/// <returns>The published content if found or null.</returns>
IPublishedContent? FindContent(Endpoint endpoint, ActionExecutingContext actionExecutingContext);
/// <summary>
/// Creates the published request for the published content.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="publishedContent">The published content.</param>
/// <returns>The published request.</returns>
Task<IPublishedRequest> CreatePublishedRequest(HttpContext httpContext, IPublishedContent publishedContent);
/// <summary>
/// Sets the route values for the published content and the controller action descriptor.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="publishedContent">The published content.</param>
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
/// <returns>Nothing.</returns>
Task SetRouteValues(HttpContext httpContext, IPublishedContent publishedContent, ControllerActionDescriptor controllerActionDescriptor);
}

View File

@@ -0,0 +1,178 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Extensions;
namespace Umbraco.Cms.Web.Common.Routing;
/// <summary>
/// This is used to setup the virtual page route so the route values and content are set for virtual pages.
/// </summary>
public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute
{
private readonly EndpointDataSource _endpointDataSource;
private readonly LinkParser _linkParser;
private readonly UriUtility _uriUtility;
private readonly IPublishedRouter _publishedRouter;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="endpointDataSource">The endpoint data source.</param>
/// <param name="linkParser">The link parser.</param>
/// <param name="uriUtility">The Uri utility.</param>
/// <param name="publishedRouter">The published router.</param>
public UmbracoVirtualPageRoute(
EndpointDataSource endpointDataSource,
LinkParser linkParser,
UriUtility uriUtility,
IPublishedRouter publishedRouter)
{
_endpointDataSource = endpointDataSource;
_linkParser = linkParser;
_uriUtility = uriUtility;
_publishedRouter = publishedRouter;
}
/// <summary>
/// This sets up the virtual page route for the current request if a mtahcing endpoint is found.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>Nothing</returns>
public async Task SetupVirtualPageRoute(HttpContext httpContext)
{
// Try and find an endpoint for the current path...
Endpoint? endpoint = _endpointDataSource.GetEndpointByPath(_linkParser, httpContext.Request.Path, out RouteValueDictionary? routeValues);
if (endpoint != null && routeValues != null)
{
ControllerActionDescriptor? controllerActionDescriptor = endpoint.GetControllerActionDescriptor();
if (controllerActionDescriptor != null)
{
Type controllerType = controllerActionDescriptor.ControllerTypeInfo.AsType();
if (controllerType != null)
{
// Get the controller for the endpoint
var controller = httpContext.RequestServices.GetRequiredService(controllerType);
// Try and find the content if this is a virtual page
IPublishedContent? publishedContent = FindContent(
endpoint,
httpContext,
routeValues,
controllerActionDescriptor,
controller);
if (publishedContent != null)
{
// If we have content then set the route values
await SetRouteValues(httpContext, publishedContent, controllerActionDescriptor);
}
}
}
}
}
/// <summary>
/// Finds the content from the custom route finder delegate or the virtual page controller.
/// Note - This creates a dummay action executing context so the FindContent method of the
/// IVirtualPageController can be called (without changing the interface contract).
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="routeValues">The route values.</param>
/// <param name="controllerActionDescriptor">The action descriptor.</param>
/// <param name="controller">The controller.</param>
/// <returns></returns>
public IPublishedContent? FindContent(
Endpoint endpoint,
HttpContext httpContext,
RouteValueDictionary routeValues,
ControllerActionDescriptor controllerActionDescriptor,
object controller)
{
var actionExecutingContext = new ActionExecutingContext(
new ActionContext(
httpContext,
new RouteData(routeValues),
controllerActionDescriptor),
filters: new List<IFilterMetadata>(),
actionArguments: new Dictionary<string, object?>(),
controller: controller);
return FindContent(endpoint, actionExecutingContext);
}
/// <summary>
/// Finds the content from the custom route finder delegate or the virtual page controller.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="actionExecutingContext">The action executing context.</param>
/// <returns>The published content if found or null.</returns>
public IPublishedContent? FindContent(Endpoint endpoint, ActionExecutingContext actionExecutingContext)
{
// Check if there is any delegate in the metadata of the route, this
// will occur when using the ForUmbraco method during routing.
CustomRouteContentFinderDelegate? contentFinder =
endpoint?.Metadata.OfType<CustomRouteContentFinderDelegate>().FirstOrDefault();
if (contentFinder != null)
{
return contentFinder.FindContent(actionExecutingContext);
}
else
{
// Check if the controller is IVirtualPageController and then use that to FindContent
if (actionExecutingContext.Controller is IVirtualPageController virtualPageController)
{
return virtualPageController.FindContent(actionExecutingContext);
}
}
return null;
}
/// <summary>
/// Creates the published request for the published content.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="publishedContent">The published content.</param>
/// <returns>The published request.</returns>
public async Task<IPublishedRequest> CreatePublishedRequest(HttpContext httpContext, IPublishedContent publishedContent)
{
var originalRequestUrl = new Uri(httpContext.Request.GetEncodedUrl());
Uri cleanedUrl = _uriUtility.UriToUmbraco(originalRequestUrl);
IPublishedRequestBuilder requestBuilder = await _publishedRouter.CreateRequestAsync(cleanedUrl);
requestBuilder.SetPublishedContent(publishedContent);
return requestBuilder.Build();
}
/// <summary>
/// Sets the route values for the published content and the controller action descriptor.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="publishedContent">The published content.</param>
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
/// <returns>Nothing.</returns>
public async Task SetRouteValues(HttpContext httpContext, IPublishedContent publishedContent, ControllerActionDescriptor controllerActionDescriptor)
{
IPublishedRequest publishedRequest = await CreatePublishedRequest(httpContext, publishedContent);
var umbracoRouteValues = new UmbracoRouteValues(
publishedRequest,
controllerActionDescriptor);
httpContext.Features.Set(umbracoRouteValues);
}
}