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:
@@ -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;
|
||||
}
|
||||
}
|
||||
41
src/Umbraco.Web.Common/Extensions/EndpointExtensions.cs
Normal file
41
src/Umbraco.Web.Common/Extensions/EndpointExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/Umbraco.Web.Common/Extensions/LinkParserExtensions.cs
Normal file
31
src/Umbraco.Web.Common/Extensions/LinkParserExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Umbraco.Web.Common/Routing/IUmbracoVirtualPageRoute.cs
Normal file
64
src/Umbraco.Web.Common/Routing/IUmbracoVirtualPageRoute.cs
Normal 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);
|
||||
}
|
||||
178
src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs
Normal file
178
src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user