Changes how controllers are discovered, re-uses aspnetcore to do this, rely directly on the resolved ControllerActionDescriptor since this is how routing works anyways and also saves future lookups (perf), gets the UmbracoPageResult 'working' - at least to proxy a controller execution but now we need to do the model state merging, etc...
This commit is contained in:
@@ -205,7 +205,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var routeData = new RouteData();
|
||||
httpContext.Features.Set(new UmbracoRouteValues(publishedRequest));
|
||||
httpContext.Features.Set(new UmbracoRouteValues(publishedRequest, null));
|
||||
|
||||
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers
|
||||
builder.SetPublishedContent(content);
|
||||
IPublishedRequest publishedRequest = builder.Build();
|
||||
|
||||
var routeDefinition = new UmbracoRouteValues(publishedRequest);
|
||||
var routeDefinition = new UmbracoRouteValues(publishedRequest, null);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set(routeDefinition);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
@@ -10,6 +11,7 @@ using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Extensions;
|
||||
@@ -88,12 +90,19 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
|
||||
[TestCase("Custom", "Render1", nameof(Render1Controller.Custom), true)]
|
||||
public void Matches_Controller(string action, string controller, string resultAction, bool matches)
|
||||
{
|
||||
IActionDescriptorCollectionProvider descriptors = GetActionDescriptors();
|
||||
|
||||
// TODO: Mock this more so that these tests work
|
||||
IActionSelector actionSelector = Mock.Of<IActionSelector>();
|
||||
|
||||
var query = new ControllerActionSearcher(
|
||||
new NullLogger<ControllerActionSearcher>(),
|
||||
GetActionDescriptors());
|
||||
actionSelector);
|
||||
|
||||
ControllerActionSearchResult result = query.Find<IRenderController>(controller, action);
|
||||
Assert.AreEqual(matches, result.Success);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
ControllerActionDescriptor result = query.Find<IRenderController>(httpContext, controller, action);
|
||||
Assert.IsTrue(matches == (result != null));
|
||||
if (matches)
|
||||
{
|
||||
Assert.IsTrue(result.ActionName.InvariantEquals(resultAction), "expected {0} does not match resulting action {1}", resultAction, result.ActionName);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -73,8 +75,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
|
||||
private UmbracoRouteValues GetRouteValues(IPublishedRequest request)
|
||||
=> new UmbracoRouteValues(
|
||||
request,
|
||||
ControllerExtensions.GetControllerName<TestController>(),
|
||||
typeof(TestController));
|
||||
new ControllerActionDescriptor
|
||||
{
|
||||
ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
|
||||
ControllerName = ControllerExtensions.GetControllerName<TestController>()
|
||||
});
|
||||
|
||||
private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request)
|
||||
=> Mock.Of<IUmbracoRouteValuesFactory>(x => x.Create(It.IsAny<HttpContext>(), It.IsAny<IPublishedRequest>()) == GetRouteValues(request));
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
|
||||
new UmbracoFeatures(),
|
||||
new ControllerActionSearcher(
|
||||
new NullLogger<ControllerActionSearcher>(),
|
||||
Mock.Of<IActionDescriptorCollectionProvider>()),
|
||||
Mock.Of<IActionSelector>()),
|
||||
publishedRouter.Object);
|
||||
|
||||
return factory;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Web.Common.Controllers;
|
||||
@@ -21,29 +22,25 @@ namespace Umbraco.Web.Common.Routing
|
||||
/// </summary>
|
||||
public UmbracoRouteValues(
|
||||
IPublishedRequest publishedRequest,
|
||||
string controllerName = null,
|
||||
Type controllerType = null,
|
||||
string actionName = DefaultActionName,
|
||||
ControllerActionDescriptor controllerActionDescriptor,
|
||||
string templateName = null,
|
||||
bool hasHijackedRoute = false)
|
||||
{
|
||||
ControllerName = controllerName ?? ControllerExtensions.GetControllerName<RenderController>();
|
||||
ControllerType = controllerType ?? typeof(RenderController);
|
||||
PublishedRequest = publishedRequest;
|
||||
ControllerActionDescriptor = controllerActionDescriptor;
|
||||
HasHijackedRoute = hasHijackedRoute;
|
||||
ActionName = actionName;
|
||||
TemplateName = templateName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the controller name
|
||||
/// </summary>
|
||||
public string ControllerName { get; }
|
||||
public string ControllerName => ControllerActionDescriptor.ControllerName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action name
|
||||
/// </summary>
|
||||
public string ActionName { get; }
|
||||
public string ActionName => ControllerActionDescriptor.ActionName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the template name
|
||||
@@ -51,9 +48,14 @@ namespace Umbraco.Web.Common.Routing
|
||||
public string TemplateName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Controller type found for routing to
|
||||
/// Gets the controller type
|
||||
/// </summary>
|
||||
public Type ControllerType { get; }
|
||||
public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Controller descriptor found for routing to
|
||||
/// </summary>
|
||||
public ControllerActionDescriptor ControllerActionDescriptor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPublishedRequest"/>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
@@ -14,7 +15,10 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Web.Common.Controllers;
|
||||
using Umbraco.Web.Common.Routing;
|
||||
using Umbraco.Web.Website.Controllers;
|
||||
using Umbraco.Web.Website.Routing;
|
||||
|
||||
namespace Umbraco.Web.Website.ActionResults
|
||||
{
|
||||
@@ -30,49 +34,65 @@ namespace Umbraco.Web.Website.ActionResults
|
||||
_profilingLogger = profilingLogger;
|
||||
}
|
||||
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
var routeData = context.RouteData;
|
||||
|
||||
ResetRouteData(routeData);
|
||||
ValidateRouteData(context);
|
||||
|
||||
IControllerFactory factory = context.HttpContext.RequestServices.GetRequiredService<IControllerFactory>();
|
||||
Controller controller = null;
|
||||
|
||||
if (!(context is ControllerContext controllerContext))
|
||||
UmbracoRouteValues umbracoRouteValues = context.HttpContext.Features.Get<UmbracoRouteValues>();
|
||||
if (umbracoRouteValues == null)
|
||||
{
|
||||
return Task.FromCanceled(CancellationToken.None);
|
||||
throw new InvalidOperationException($"Can only use {nameof(UmbracoPageResult)} in the context of an Http POST when using a {nameof(SurfaceController)} form");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
controller = CreateController(controllerContext, factory);
|
||||
// Change the route values back to the original request vals
|
||||
context.RouteData.Values[UmbracoRouteValueTransformer.ControllerToken] = umbracoRouteValues.ControllerName;
|
||||
context.RouteData.Values[UmbracoRouteValueTransformer.ActionToken] = umbracoRouteValues.ActionName;
|
||||
|
||||
CopyControllerData(controllerContext, controller);
|
||||
// Create a new context and excute the original controller
|
||||
|
||||
ExecuteControllerAction(controllerContext, controller);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupController(controllerContext, controller, factory);
|
||||
}
|
||||
// TODO: We need to take into account temp data, view data, etc... all like what we used to do below
|
||||
// so that validation stuff gets carried accross
|
||||
|
||||
return Task.CompletedTask;
|
||||
var renderActionContext = new ActionContext(context.HttpContext, context.RouteData, umbracoRouteValues.ControllerActionDescriptor);
|
||||
IActionInvokerFactory actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
|
||||
IActionInvoker actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext);
|
||||
await ExecuteControllerAction(actionInvoker);
|
||||
|
||||
//ResetRouteData(context.RouteData);
|
||||
//ValidateRouteData(context);
|
||||
|
||||
//IControllerFactory factory = context.HttpContext.RequestServices.GetRequiredService<IControllerFactory>();
|
||||
//Controller controller = null;
|
||||
|
||||
//if (!(context is ControllerContext controllerContext))
|
||||
//{
|
||||
// // TODO: Better to throw since this is not expected?
|
||||
// return Task.FromCanceled(CancellationToken.None);
|
||||
//}
|
||||
|
||||
//try
|
||||
//{
|
||||
// controller = CreateController(controllerContext, factory);
|
||||
|
||||
// CopyControllerData(controllerContext, controller);
|
||||
|
||||
// ExecuteControllerAction(controllerContext, controller);
|
||||
//}
|
||||
//finally
|
||||
//{
|
||||
// CleanupController(controllerContext, controller, factory);
|
||||
//}
|
||||
|
||||
//return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the controller action
|
||||
/// </summary>
|
||||
private void ExecuteControllerAction(ControllerContext context, Controller controller)
|
||||
private async Task ExecuteControllerAction(IActionInvoker actionInvoker)
|
||||
{
|
||||
using (_profilingLogger.TraceDuration<UmbracoPageResult>("Executing Umbraco RouteDefinition controller", "Finished"))
|
||||
{
|
||||
//TODO I do not think this will work, We need to test this, when we can, in the .NET Core executable.
|
||||
var aec = new ActionExecutingContext(context, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller);
|
||||
var actionExecutedDelegate = CreateActionExecutedDelegate(aec);
|
||||
|
||||
controller.OnActionExecutionAsync(aec, actionExecutedDelegate);
|
||||
await actionInvoker.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,29 +108,6 @@ namespace Umbraco.Web.Website.ActionResults
|
||||
return () => Task.FromResult(actionExecutedContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we
|
||||
/// need to revert these values back to nothing in order for the normal page to render again.
|
||||
/// </summary>
|
||||
private static void ResetRouteData(RouteData routeData)
|
||||
{
|
||||
routeData.DataTokens["area"] = null;
|
||||
routeData.DataTokens["Namespaces"] = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that the current page execution is not being handled by the normal umbraco routing system
|
||||
/// </summary>
|
||||
private static void ValidateRouteData(ActionContext actionContext)
|
||||
{
|
||||
UmbracoRouteValues umbracoRouteValues = actionContext.HttpContext.Features.Get<UmbracoRouteValues>();
|
||||
if (umbracoRouteValues == null)
|
||||
{
|
||||
throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name +
|
||||
" in the context of an Http POST when using a SurfaceController form");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure ModelState, ViewData and TempData is copied across
|
||||
/// </summary>
|
||||
@@ -118,7 +115,7 @@ namespace Umbraco.Web.Website.ActionResults
|
||||
{
|
||||
controller.ViewData.ModelState.Merge(context.ModelState);
|
||||
|
||||
foreach (var d in controller.ViewData)
|
||||
foreach (KeyValuePair<string, object> d in controller.ViewData)
|
||||
{
|
||||
controller.ViewData[d.Key] = d.Value;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Umbraco.Web.Website.Controllers
|
||||
// TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters
|
||||
// [MergeModelStateToChildAction]
|
||||
// [MergeParentContextViewData]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public abstract class SurfaceController : PluginController
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -78,7 +78,8 @@ namespace Umbraco.Extensions
|
||||
umbrcoContext.PublishedRequest.PublishedContent.Id);
|
||||
return new HtmlString(htmlBadge);
|
||||
}
|
||||
return new HtmlString(string.Empty);
|
||||
|
||||
return HtmlString.Empty;
|
||||
|
||||
}
|
||||
|
||||
@@ -243,14 +244,55 @@ namespace Umbraco.Extensions
|
||||
return htmlHelper.ActionLink(actionName, metaData.ControllerName, routeVals);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outputs the hidden html input field for Surface Controller route information
|
||||
/// </summary>
|
||||
/// <typeparam name="TSurface">The <see cref="SurfaceController"/> type</typeparam>
|
||||
/// <remarks>
|
||||
/// Typically not used directly because BeginUmbracoForm automatically outputs this value when routing
|
||||
/// for surface controllers. But this could be used in case a form tag is manually created.
|
||||
/// </remarks>
|
||||
public static IHtmlContent SurfaceControllerHiddenInput<TSurface>(
|
||||
this IHtmlHelper htmlHelper,
|
||||
string controllerAction,
|
||||
string area,
|
||||
object additionalRouteVals = null)
|
||||
where TSurface : SurfaceController
|
||||
{
|
||||
var inputField = GetSurfaceControllerHiddenInput(
|
||||
GetRequiredService<IDataProtectionProvider>(htmlHelper),
|
||||
ControllerExtensions.GetControllerName<TSurface>(),
|
||||
controllerAction,
|
||||
area,
|
||||
additionalRouteVals);
|
||||
|
||||
return new HtmlString(inputField);
|
||||
}
|
||||
|
||||
private static string GetSurfaceControllerHiddenInput(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
string controllerName,
|
||||
string controllerAction,
|
||||
string area,
|
||||
object additionalRouteVals = null)
|
||||
{
|
||||
var encryptedString = EncryptionHelper.CreateEncryptedRouteString(
|
||||
dataProtectionProvider,
|
||||
controllerName,
|
||||
controllerAction,
|
||||
area,
|
||||
additionalRouteVals);
|
||||
|
||||
return "<input name=\"ufprt\" type=\"hidden\" value=\"" + encryptedString + "\" />";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for rendering out the Form for BeginUmbracoForm
|
||||
/// </summary>
|
||||
internal class UmbracoForm : MvcForm
|
||||
{
|
||||
private readonly ViewContext _viewContext;
|
||||
private readonly string _encryptedString;
|
||||
private readonly string _controllerName;
|
||||
private readonly string _surfaceControllerInput;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoForm"/> class.
|
||||
@@ -265,25 +307,23 @@ namespace Umbraco.Extensions
|
||||
: base(viewContext, htmlEncoder)
|
||||
{
|
||||
_viewContext = viewContext;
|
||||
_controllerName = controllerName;
|
||||
_encryptedString = EncryptionHelper.CreateEncryptedRouteString(GetRequiredService<IDataProtectionProvider>(viewContext), controllerName, controllerAction, area, additionalRouteVals);
|
||||
_surfaceControllerInput = GetSurfaceControllerHiddenInput(
|
||||
GetRequiredService<IDataProtectionProvider>(viewContext),
|
||||
controllerName,
|
||||
controllerAction,
|
||||
area,
|
||||
additionalRouteVals);
|
||||
}
|
||||
|
||||
protected override void GenerateEndForm()
|
||||
{
|
||||
// Detect if the call is targeting UmbRegisterController/UmbProfileController/UmbLoginStatusController/UmbLoginController and if it is we automatically output a AntiForgeryToken()
|
||||
// We have a controllerName and area so we can match
|
||||
if (_controllerName == "UmbRegister"
|
||||
|| _controllerName == "UmbProfile"
|
||||
|| _controllerName == "UmbLoginStatus"
|
||||
|| _controllerName == "UmbLogin")
|
||||
{
|
||||
IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
|
||||
_viewContext.Writer.Write(antiforgery.GetHtml(_viewContext.HttpContext).ToString());
|
||||
}
|
||||
// Always output an anti-forgery token
|
||||
IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
|
||||
IHtmlContent antiforgeryHtml = antiforgery.GetHtml(_viewContext.HttpContext);
|
||||
_viewContext.Writer.Write(antiforgeryHtml.ToHtmlString());
|
||||
|
||||
// write out the hidden surface form routes
|
||||
_viewContext.Writer.Write("<input name=\"ufprt\" type=\"hidden\" value=\"" + _encryptedString + "\" />");
|
||||
_viewContext.Writer.Write(_surfaceControllerInput);
|
||||
|
||||
base.GenerateEndForm();
|
||||
}
|
||||
@@ -403,7 +443,7 @@ namespace Umbraco.Extensions
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName));
|
||||
}
|
||||
|
||||
return html.BeginUmbracoForm(action, controllerName, "", additionalRouteVals, htmlAttributes);
|
||||
return html.BeginUmbracoForm(action, controllerName, string.Empty, additionalRouteVals, htmlAttributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Web.Website.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// The result from querying a controller/action in the existing routes
|
||||
/// </summary>
|
||||
public class ControllerActionSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControllerActionSearchResult"/> class.
|
||||
/// </summary>
|
||||
private ControllerActionSearchResult(
|
||||
bool success,
|
||||
string controllerName,
|
||||
Type controllerType,
|
||||
string actionName)
|
||||
{
|
||||
Success = success;
|
||||
ControllerName = controllerName;
|
||||
ControllerType = controllerType;
|
||||
ActionName = actionName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControllerActionSearchResult"/> class.
|
||||
/// </summary>
|
||||
public ControllerActionSearchResult(
|
||||
string controllerName,
|
||||
Type controllerType,
|
||||
string actionName)
|
||||
: this(true, controllerName, controllerType, actionName)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the route could be hijacked
|
||||
/// </summary>
|
||||
public bool Success { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Controller name
|
||||
/// </summary>
|
||||
public string ControllerName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Controller type
|
||||
/// </summary>
|
||||
public Type ControllerType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Acton name
|
||||
/// </summary>
|
||||
public string ActionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a failed result
|
||||
/// </summary>
|
||||
public static ControllerActionSearchResult Failed() => new ControllerActionSearchResult(false, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Web.Common.Controllers;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Umbraco.Web.Website.Routing
|
||||
public class ControllerActionSearcher : IControllerActionSearcher
|
||||
{
|
||||
private readonly ILogger<ControllerActionSearcher> _logger;
|
||||
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
||||
private readonly IActionSelector _actionSelector;
|
||||
private const string DefaultActionName = nameof(RenderController.Index);
|
||||
|
||||
/// <summary>
|
||||
@@ -24,78 +24,69 @@ namespace Umbraco.Web.Website.Routing
|
||||
/// </summary>
|
||||
public ControllerActionSearcher(
|
||||
ILogger<ControllerActionSearcher> logger,
|
||||
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
|
||||
IActionSelector actionSelector)
|
||||
{
|
||||
_logger = logger;
|
||||
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
||||
_actionSelector = actionSelector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a custom controller can hijack the current route
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The controller type to find</typeparam>
|
||||
public ControllerActionSearchResult Find<T>(string controller, string action)
|
||||
public ControllerActionDescriptor Find<T>(HttpContext httpContext, string controller, string action)
|
||||
{
|
||||
IReadOnlyList<ControllerActionDescriptor> candidates = FindControllerCandidates(controller, action, DefaultActionName);
|
||||
IReadOnlyList<ControllerActionDescriptor> candidates = FindControllerCandidates<T>(httpContext, controller, action, DefaultActionName);
|
||||
|
||||
// check if there's a custom controller assigned, base on the document type alias.
|
||||
var customControllerCandidates = candidates.Where(x => x.ControllerName.InvariantEquals(controller)).ToList();
|
||||
|
||||
// check if that custom controller exists
|
||||
if (customControllerCandidates.Count > 0)
|
||||
if (candidates.Count > 0)
|
||||
{
|
||||
ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0];
|
||||
|
||||
// ensure the controller is of type T and ControllerBase
|
||||
if (TypeHelper.IsTypeAssignableFrom<T>(controllerDescriptor.ControllerTypeInfo)
|
||||
&& TypeHelper.IsTypeAssignableFrom<ControllerBase>(controllerDescriptor.ControllerTypeInfo))
|
||||
{
|
||||
// now check if the custom action matches
|
||||
var resultingAction = DefaultActionName;
|
||||
if (action != null)
|
||||
{
|
||||
var found = customControllerCandidates.FirstOrDefault(x => x.ActionName.InvariantEquals(action))?.ActionName;
|
||||
if (found != null)
|
||||
{
|
||||
resultingAction = found;
|
||||
}
|
||||
}
|
||||
|
||||
// it's a hijacked route with a custom controller, so return the the values
|
||||
return new ControllerActionSearchResult(
|
||||
controllerDescriptor.ControllerName,
|
||||
controllerDescriptor.ControllerTypeInfo,
|
||||
resultingAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.",
|
||||
controller,
|
||||
controllerDescriptor.ControllerTypeInfo.FullName,
|
||||
typeof(T).FullName,
|
||||
typeof(ControllerBase).FullName);
|
||||
|
||||
// we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults
|
||||
// that have already been set above.
|
||||
}
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
return ControllerActionSearchResult.Failed();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a list of controller candidates that match the custom controller and action names
|
||||
/// </summary>
|
||||
private IReadOnlyList<ControllerActionDescriptor> FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName)
|
||||
private IReadOnlyList<ControllerActionDescriptor> FindControllerCandidates<T>(
|
||||
HttpContext httpContext,
|
||||
string customControllerName,
|
||||
string customActionName,
|
||||
string defaultActionName)
|
||||
{
|
||||
var descriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items
|
||||
// Use aspnetcore's IActionSelector to do the finding since it uses an optimized cache lookup
|
||||
var routeValues = new RouteValueDictionary
|
||||
{
|
||||
[UmbracoRouteValueTransformer.ControllerToken] = customControllerName,
|
||||
[UmbracoRouteValueTransformer.ActionToken] = customActionName, // first try to find the custom action
|
||||
};
|
||||
var routeData = new RouteData(routeValues);
|
||||
var routeContext = new RouteContext(httpContext)
|
||||
{
|
||||
RouteData = routeData
|
||||
};
|
||||
|
||||
// try finding candidates for the custom action
|
||||
var candidates = _actionSelector.SelectCandidates(routeContext)
|
||||
.Cast<ControllerActionDescriptor>()
|
||||
.Where(x => x.ControllerName.InvariantEquals(customControllerName)
|
||||
&& (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName))))
|
||||
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(x.ControllerTypeInfo))
|
||||
.ToList();
|
||||
|
||||
return descriptors;
|
||||
if (candidates.Count > 0)
|
||||
{
|
||||
// return them if found
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// now find for the default action since we couldn't find the custom one
|
||||
routeValues[UmbracoRouteValueTransformer.ActionToken] = defaultActionName;
|
||||
candidates = _actionSelector.SelectCandidates(routeContext)
|
||||
.Cast<ControllerActionDescriptor>()
|
||||
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(x.ControllerTypeInfo))
|
||||
.ToList();
|
||||
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
namespace Umbraco.Web.Website.Routing
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
|
||||
namespace Umbraco.Web.Website.Routing
|
||||
{
|
||||
public interface IControllerActionSearcher
|
||||
{
|
||||
ControllerActionSearchResult Find<T>(string controller, string action);
|
||||
ControllerActionDescriptor Find<T>(HttpContext httpContext, string controller, string action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -45,8 +46,9 @@ namespace Umbraco.Web.Website.Routing
|
||||
private readonly IRoutableDocumentFilter _routableDocumentFilter;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
private readonly IControllerActionSearcher _controllerActionSearcher;
|
||||
private const string ControllerToken = "controller";
|
||||
private const string ActionToken = "action";
|
||||
|
||||
internal const string ControllerToken = "controller";
|
||||
internal const string ActionToken = "action";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer"/> class.
|
||||
@@ -197,9 +199,9 @@ namespace Umbraco.Web.Website.Routing
|
||||
values[ControllerToken] = postedInfo.ControllerName;
|
||||
values[ActionToken] = postedInfo.ActionName;
|
||||
|
||||
ControllerActionSearchResult surfaceControllerQueryResult = _controllerActionSearcher.Find<SurfaceController>(postedInfo.ControllerName, postedInfo.ActionName);
|
||||
ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find<SurfaceController>(httpContext, postedInfo.ControllerName, postedInfo.ActionName);
|
||||
|
||||
if (surfaceControllerQueryResult == null || !surfaceControllerQueryResult.Success)
|
||||
if (surfaceControllerDescriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Strings;
|
||||
@@ -24,6 +25,7 @@ namespace Umbraco.Web.Website.Routing
|
||||
private readonly IControllerActionSearcher _controllerActionSearcher;
|
||||
private readonly IPublishedRouter _publishedRouter;
|
||||
private readonly Lazy<string> _defaultControllerName;
|
||||
private readonly Lazy<ControllerActionDescriptor> _defaultControllerDescriptor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoRouteValuesFactory"/> class.
|
||||
@@ -41,6 +43,20 @@ namespace Umbraco.Web.Website.Routing
|
||||
_controllerActionSearcher = controllerActionSearcher;
|
||||
_publishedRouter = publishedRouter;
|
||||
_defaultControllerName = new Lazy<string>(() => ControllerExtensions.GetControllerName(_renderingDefaults.DefaultControllerType));
|
||||
_defaultControllerDescriptor = new Lazy<ControllerActionDescriptor>(() =>
|
||||
{
|
||||
ControllerActionDescriptor descriptor = _controllerActionSearcher.Find<IRenderController>(
|
||||
new DefaultHttpContext(), // this actually makes no difference for this method
|
||||
DefaultControllerName,
|
||||
UmbracoRouteValues.DefaultActionName);
|
||||
|
||||
if (descriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No controller/action found by name {DefaultControllerName}.{UmbracoRouteValues.DefaultActionName}");
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,8 +77,6 @@ namespace Umbraco.Web.Website.Routing
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
Type defaultControllerType = _renderingDefaults.DefaultControllerType;
|
||||
|
||||
string customActionName = null;
|
||||
|
||||
// check that a template is defined), if it doesn't and there is a hijacked route it will just route
|
||||
@@ -75,16 +89,15 @@ namespace Umbraco.Web.Website.Routing
|
||||
customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper);
|
||||
}
|
||||
|
||||
// creates the default route definition which maps to the 'UmbracoController' controller
|
||||
// The default values for the default controller/action
|
||||
var def = new UmbracoRouteValues(
|
||||
request,
|
||||
DefaultControllerName,
|
||||
defaultControllerType,
|
||||
_defaultControllerDescriptor.Value,
|
||||
templateName: customActionName);
|
||||
|
||||
def = CheckHijackedRoute(def);
|
||||
def = CheckHijackedRoute(httpContext, def);
|
||||
|
||||
def = CheckNoTemplate(def);
|
||||
def = CheckNoTemplate(httpContext, def);
|
||||
|
||||
return def;
|
||||
}
|
||||
@@ -92,21 +105,19 @@ namespace Umbraco.Web.Website.Routing
|
||||
/// <summary>
|
||||
/// Check if the route is hijacked and return new route values
|
||||
/// </summary>
|
||||
private UmbracoRouteValues CheckHijackedRoute(UmbracoRouteValues def)
|
||||
private UmbracoRouteValues CheckHijackedRoute(HttpContext httpContext, UmbracoRouteValues def)
|
||||
{
|
||||
IPublishedRequest request = def.PublishedRequest;
|
||||
|
||||
var customControllerName = request.PublishedContent?.ContentType?.Alias;
|
||||
if (customControllerName != null)
|
||||
{
|
||||
ControllerActionSearchResult hijackedResult = _controllerActionSearcher.Find<IRenderController>(customControllerName, def.TemplateName);
|
||||
if (hijackedResult.Success)
|
||||
ControllerActionDescriptor descriptor = _controllerActionSearcher.Find<IRenderController>(httpContext, customControllerName, def.TemplateName);
|
||||
if (descriptor != null)
|
||||
{
|
||||
return new UmbracoRouteValues(
|
||||
request,
|
||||
hijackedResult.ControllerName,
|
||||
hijackedResult.ControllerType,
|
||||
hijackedResult.ActionName,
|
||||
descriptor,
|
||||
def.TemplateName,
|
||||
true);
|
||||
}
|
||||
@@ -118,7 +129,7 @@ namespace Umbraco.Web.Website.Routing
|
||||
/// <summary>
|
||||
/// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline again for last chance finders
|
||||
/// </summary>
|
||||
private UmbracoRouteValues CheckNoTemplate(UmbracoRouteValues def)
|
||||
private UmbracoRouteValues CheckNoTemplate(HttpContext httpContext, UmbracoRouteValues def)
|
||||
{
|
||||
IPublishedRequest request = def.PublishedRequest;
|
||||
|
||||
@@ -147,15 +158,13 @@ namespace Umbraco.Web.Website.Routing
|
||||
|
||||
def = new UmbracoRouteValues(
|
||||
request,
|
||||
def.ControllerName,
|
||||
def.ControllerType,
|
||||
def.ActionName,
|
||||
def.ControllerActionDescriptor,
|
||||
def.TemplateName);
|
||||
|
||||
// if the content has changed, we must then again check for hijacked routes
|
||||
if (content != request.PublishedContent)
|
||||
{
|
||||
def = CheckHijackedRoute(def);
|
||||
def = CheckHijackedRoute(httpContext, def);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user