diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs index f4e3aed39f..837a0059f4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs @@ -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(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index 53ae713564..21143662cb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -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); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs index 9556640364..3437091663 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs @@ -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(); + var query = new ControllerActionSearcher( new NullLogger(), - GetActionDescriptors()); + actionSelector); - ControllerActionSearchResult result = query.Find(controller, action); - Assert.AreEqual(matches, result.Success); + var httpContext = new DefaultHttpContext(); + + ControllerActionDescriptor result = query.Find(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); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs index e3147220bb..61918558ac 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs @@ -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(), - typeof(TestController)); + new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(TestController).GetTypeInfo(), + ControllerName = ControllerExtensions.GetControllerName() + }); private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request) => Mock.Of(x => x.Create(It.IsAny(), It.IsAny()) == GetRouteValues(request)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs index 2c8dd3b395..a08eb1faa1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs @@ -41,7 +41,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing new UmbracoFeatures(), new ControllerActionSearcher( new NullLogger(), - Mock.Of()), + Mock.Of()), publishedRouter.Object); return factory; diff --git a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs index 8622bc689e..9ac8c1d2e6 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs @@ -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 /// 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(); - ControllerType = controllerType ?? typeof(RenderController); PublishedRequest = publishedRequest; + ControllerActionDescriptor = controllerActionDescriptor; HasHijackedRoute = hasHijackedRoute; - ActionName = actionName; TemplateName = templateName; } /// /// Gets the controller name /// - public string ControllerName { get; } + public string ControllerName => ControllerActionDescriptor.ControllerName; /// /// Gets the action name /// - public string ActionName { get; } + public string ActionName => ControllerActionDescriptor.ActionName; /// /// Gets the template name @@ -51,9 +48,14 @@ namespace Umbraco.Web.Common.Routing public string TemplateName { get; } /// - /// Gets the Controller type found for routing to + /// Gets the controller type /// - public Type ControllerType { get; } + public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo; + + /// + /// Gets the Controller descriptor found for routing to + /// + public ControllerActionDescriptor ControllerActionDescriptor { get; } /// /// Gets the diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs index 65af4a2df5..4eb40e8e4e 100644 --- a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -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) + /// + public async Task ExecuteResultAsync(ActionContext context) { - var routeData = context.RouteData; - - ResetRouteData(routeData); - ValidateRouteData(context); - - IControllerFactory factory = context.HttpContext.RequestServices.GetRequiredService(); - Controller controller = null; - - if (!(context is ControllerContext controllerContext)) + UmbracoRouteValues umbracoRouteValues = context.HttpContext.Features.Get(); + 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(); + IActionInvoker actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext); + await ExecuteControllerAction(actionInvoker); + + //ResetRouteData(context.RouteData); + //ValidateRouteData(context); + + //IControllerFactory factory = context.HttpContext.RequestServices.GetRequiredService(); + //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; } /// /// Executes the controller action /// - private void ExecuteControllerAction(ControllerContext context, Controller controller) + private async Task ExecuteControllerAction(IActionInvoker actionInvoker) { using (_profilingLogger.TraceDuration("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(), new Dictionary(), 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); } - /// - /// 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. - /// - private static void ResetRouteData(RouteData routeData) - { - routeData.DataTokens["area"] = null; - routeData.DataTokens["Namespaces"] = null; - } - - /// - /// Validate that the current page execution is not being handled by the normal umbraco routing system - /// - private static void ValidateRouteData(ActionContext actionContext) - { - UmbracoRouteValues umbracoRouteValues = actionContext.HttpContext.Features.Get(); - if (umbracoRouteValues == null) - { - throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + - " in the context of an Http POST when using a SurfaceController form"); - } - } - /// /// Ensure ModelState, ViewData and TempData is copied across /// @@ -118,7 +115,7 @@ namespace Umbraco.Web.Website.ActionResults { controller.ViewData.ModelState.Merge(context.ModelState); - foreach (var d in controller.ViewData) + foreach (KeyValuePair d in controller.ViewData) { controller.ViewData[d.Key] = d.Value; } diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index ceb8a012cf..f7098e316d 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -21,6 +21,7 @@ namespace Umbraco.Web.Website.Controllers // TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters // [MergeModelStateToChildAction] // [MergeParentContextViewData] + [AutoValidateAntiforgeryToken] public abstract class SurfaceController : PluginController { /// diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index ac6154f645..1c0e417047 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -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); } + /// + /// Outputs the hidden html input field for Surface Controller route information + /// + /// The type + /// + /// 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. + /// + public static IHtmlContent SurfaceControllerHiddenInput( + this IHtmlHelper htmlHelper, + string controllerAction, + string area, + object additionalRouteVals = null) + where TSurface : SurfaceController + { + var inputField = GetSurfaceControllerHiddenInput( + GetRequiredService(htmlHelper), + ControllerExtensions.GetControllerName(), + 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 ""; + } + /// /// Used for rendering out the Form for BeginUmbracoForm /// internal class UmbracoForm : MvcForm { private readonly ViewContext _viewContext; - private readonly string _encryptedString; - private readonly string _controllerName; + private readonly string _surfaceControllerInput; /// /// Initializes a new instance of the class. @@ -265,25 +307,23 @@ namespace Umbraco.Extensions : base(viewContext, htmlEncoder) { _viewContext = viewContext; - _controllerName = controllerName; - _encryptedString = EncryptionHelper.CreateEncryptedRouteString(GetRequiredService(viewContext), controllerName, controllerAction, area, additionalRouteVals); + _surfaceControllerInput = GetSurfaceControllerHiddenInput( + GetRequiredService(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(); - _viewContext.Writer.Write(antiforgery.GetHtml(_viewContext.HttpContext).ToString()); - } + // Always output an anti-forgery token + IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService(); + IHtmlContent antiforgeryHtml = antiforgery.GetHtml(_viewContext.HttpContext); + _viewContext.Writer.Write(antiforgeryHtml.ToHtmlString()); // write out the hidden surface form routes - _viewContext.Writer.Write(""); + _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); } /// diff --git a/src/Umbraco.Web.Website/Routing/ControllerActionSearchResult.cs b/src/Umbraco.Web.Website/Routing/ControllerActionSearchResult.cs deleted file mode 100644 index 2c2f4802df..0000000000 --- a/src/Umbraco.Web.Website/Routing/ControllerActionSearchResult.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; - -namespace Umbraco.Web.Website.Routing -{ - /// - /// The result from querying a controller/action in the existing routes - /// - public class ControllerActionSearchResult - { - /// - /// Initializes a new instance of the class. - /// - private ControllerActionSearchResult( - bool success, - string controllerName, - Type controllerType, - string actionName) - { - Success = success; - ControllerName = controllerName; - ControllerType = controllerType; - ActionName = actionName; - } - - /// - /// Initializes a new instance of the class. - /// - public ControllerActionSearchResult( - string controllerName, - Type controllerType, - string actionName) - : this(true, controllerName, controllerType, actionName) - { - } - - /// - /// Gets a value indicating whether the route could be hijacked - /// - public bool Success { get; } - - /// - /// Gets the Controller name - /// - public string ControllerName { get; } - - /// - /// Gets the Controller type - /// - public Type ControllerType { get; } - - /// - /// Gets the Acton name - /// - public string ActionName { get; } - - /// - /// Returns a failed result - /// - public static ControllerActionSearchResult Failed() => new ControllerActionSearchResult(false, null, null, null); - } -} diff --git a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs index f38c4676c8..81b09cf3a3 100644 --- a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs @@ -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 _logger; - private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private readonly IActionSelector _actionSelector; private const string DefaultActionName = nameof(RenderController.Index); /// @@ -24,78 +24,69 @@ namespace Umbraco.Web.Website.Routing /// public ControllerActionSearcher( ILogger logger, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + IActionSelector actionSelector) { _logger = logger; - _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + _actionSelector = actionSelector; } /// /// Determines if a custom controller can hijack the current route /// /// The controller type to find - public ControllerActionSearchResult Find(string controller, string action) + public ControllerActionDescriptor Find(HttpContext httpContext, string controller, string action) { - IReadOnlyList candidates = FindControllerCandidates(controller, action, DefaultActionName); + IReadOnlyList candidates = FindControllerCandidates(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(controllerDescriptor.ControllerTypeInfo) - && TypeHelper.IsTypeAssignableFrom(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; } /// /// Return a list of controller candidates that match the custom controller and action names /// - private IReadOnlyList FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName) + private IReadOnlyList FindControllerCandidates( + 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() - .Where(x => x.ControllerName.InvariantEquals(customControllerName) - && (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName)))) + .Where(x => TypeHelper.IsTypeAssignableFrom(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() + .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) + .ToList(); + + return candidates; } } } diff --git a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs index 36b4382cc2..6236a2b8f0 100644 --- a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs @@ -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(string controller, string action); + ControllerActionDescriptor Find(HttpContext httpContext, string controller, string action); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index a4ab61b3cc..2ee9288a60 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -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"; /// /// Initializes a new instance of the class. @@ -197,9 +199,9 @@ namespace Umbraco.Web.Website.Routing values[ControllerToken] = postedInfo.ControllerName; values[ActionToken] = postedInfo.ActionName; - ControllerActionSearchResult surfaceControllerQueryResult = _controllerActionSearcher.Find(postedInfo.ControllerName, postedInfo.ActionName); + ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find(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); } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index 9d75733f1f..000a3e252a 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -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 _defaultControllerName; + private readonly Lazy _defaultControllerDescriptor; /// /// Initializes a new instance of the class. @@ -41,6 +43,20 @@ namespace Umbraco.Web.Website.Routing _controllerActionSearcher = controllerActionSearcher; _publishedRouter = publishedRouter; _defaultControllerName = new Lazy(() => ControllerExtensions.GetControllerName(_renderingDefaults.DefaultControllerType)); + _defaultControllerDescriptor = new Lazy(() => + { + ControllerActionDescriptor descriptor = _controllerActionSearcher.Find( + 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; + }); } /// @@ -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 /// /// Check if the route is hijacked and return new route values /// - 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(customControllerName, def.TemplateName); - if (hijackedResult.Success) + ControllerActionDescriptor descriptor = _controllerActionSearcher.Find(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 /// /// 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 /// - 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); } }