From bc6768101ea6cfecdc45d2d8ea604b6f3b2a8590 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Jan 2021 02:10:13 +1100 Subject: [PATCH] Splits up UmbracoRouteValueTransformer and trying to figure out this hijacked route conundrum with no template :( --- src/Umbraco.Core/Routing/PublishedRouter.cs | 23 ++- .../PublishedContent/PublishedRouterTests.cs | 4 +- .../Controllers/RenderController.cs | 43 ++++- .../UmbracoBuilderExtensions.cs | 2 + .../Routing/HijackedRouteEvaluator.cs | 90 +++++++++++ .../Routing/HijackedRouteResult.cs | 50 ++++++ .../Routing/IUmbracoRouteValuesFactory.cs | 18 +++ .../Routing/UmbracoRouteValueTransformer.cs | 153 +----------------- .../Routing/UmbracoRouteValuesFactory.cs | 135 ++++++++++++++++ 9 files changed, 351 insertions(+), 167 deletions(-) create mode 100644 src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs create mode 100644 src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs create mode 100644 src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs create mode 100644 src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index ae77abe4a7..a0f7fe6344 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -177,12 +177,11 @@ namespace Umbraco.Web.Routing // to find out the appropriate template // complete the PCR and assign the remaining values - return ConfigureRequest(request); + return BuildRequest(request); } /// - /// Called by PrepareRequest once everything has been discovered, resolved and assigned to the PCR. This method - /// finalizes the PCR with the values assigned. + /// This method finalizes/builds the PCR with the values assigned. /// /// /// Returns false if the request was not successfully configured @@ -191,15 +190,15 @@ namespace Umbraco.Web.Routing /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values /// but need to finalize it themselves. /// - internal IPublishedRequest ConfigureRequest(IPublishedRequestBuilder frequest) + internal IPublishedRequest BuildRequest(IPublishedRequestBuilder frequest) { + IPublishedRequest result = frequest.Build(); + if (!frequest.HasPublishedContent()) { - return frequest.Build(); + return result; } - IPublishedRequest result = frequest.Build(); - // set the culture -- again, 'cos it might have changed in the event handler SetVariationContext(result.Culture); @@ -388,7 +387,7 @@ namespace Umbraco.Web.Routing // so internal redirect, 404, etc has precedence over redirect // handle not-found, redirects, access... - HandlePublishedContent(request, foundContentByFinders); + HandlePublishedContent(request); // find a template FindTemplate(request, foundContentByFinders); @@ -425,12 +424,11 @@ namespace Umbraco.Web.Routing /// Handles the published content (if any). /// /// The request builder. - /// If the content was found by the finders, before anything such as 404, redirect... took place. /// /// Handles "not found", internal redirects, access validation... /// things that must be handled in one place because they can create loops /// - private void HandlePublishedContent(IPublishedRequestBuilder request, bool contentFoundByFinders) + private void HandlePublishedContent(IPublishedRequestBuilder request) { // because these might loop, we have to have some sort of infinite loop detection int i = 0, j = 0; @@ -457,7 +455,7 @@ namespace Umbraco.Web.Routing // follow internal redirects as long as it's not running out of control ie infinite loop of some sort j = 0; - while (FollowInternalRedirects(request, contentFoundByFinders) && j++ < maxLoop) + while (FollowInternalRedirects(request) && j++ < maxLoop) { } // we're running out of control @@ -490,13 +488,12 @@ namespace Umbraco.Web.Routing /// Follows internal redirections through the umbracoInternalRedirectId document property. /// /// The request builder. - /// If the content was found by the finders, before anything such as 404, redirect... took place. /// A value indicating whether redirection took place and led to a new published document. /// /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. /// As per legacy, if the redirect does not work, we just ignore it. /// - private bool FollowInternalRedirects(IPublishedRequestBuilder request, bool contentFoundByFinders) + private bool FollowInternalRedirects(IPublishedRequestBuilder request) { if (request.PublishedContent == null) { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs index 02ccc69f80..52b76a0021 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.PublishedContent var umbracoContext = GetUmbracoContext("/test"); var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext)); var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - var result = publishedRouter.ConfigureRequest(request); + var result = publishedRouter.BuildRequest(request); Assert.IsFalse(result.Success()); } @@ -37,7 +37,7 @@ namespace Umbraco.Tests.PublishedContent request.SetPublishedContent(content.Object); request.SetCulture(new CultureInfo("en-AU")); request.SetRedirect("/hello"); - var result = publishedRouter.ConfigureRequest(request); + var result = publishedRouter.BuildRequest(request); Assert.IsFalse(result.Success()); } diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 7f6d61de98..e0df571988 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -118,6 +118,15 @@ namespace Umbraco.Web.Common.Controllers /// public virtual IActionResult Index() => CurrentTemplate(new ContentModel(CurrentPage)); + /// + /// The action that renders when there is no template assigned, templates are not disabled and there is no hijacked route + /// + /// + /// This action renders even if there might be content found, but if there is no template assigned, templates are not disabled and there is no hijacked route + /// then we render the blank screen. + /// + public IActionResult NoTemplate() => GetNoTemplateResult(UmbracoRouteValues.PublishedRequest); + /// /// Before the controller executes we will handle redirects and not founds /// @@ -126,10 +135,10 @@ namespace Umbraco.Web.Common.Controllers IPublishedRequest pcr = UmbracoRouteValues.PublishedRequest; _logger.LogDebug( - "Response status: Redirect={Redirect}, Is404={Is404}, StatusCode={ResponseStatusCode}", - pcr.IsRedirect() ? (pcr.IsRedirectPermanent() ? "permanent" : "redirect") : "none", - pcr.Is404() ? "true" : "false", - pcr.ResponseStatusCode); + "Response status: Redirect={Redirect}, Is404={Is404}, StatusCode={ResponseStatusCode}", + pcr.IsRedirect() ? (pcr.IsRedirectPermanent() ? "permanent" : "redirect") : "none", + pcr.Is404() ? "true" : "false", + pcr.ResponseStatusCode); UmbracoRouteResult routeStatus = pcr.GetRouteResult(); switch (routeStatus) @@ -142,9 +151,8 @@ namespace Umbraco.Web.Common.Controllers : Redirect(pcr.RedirectUrl); break; case UmbracoRouteResult.NotFound: - // set the redirect result and do not call next to short circuit - context.Result = new PublishedContentNotFoundResult(UmbracoContext); + context.Result = GetNoTemplateResult(pcr); break; case UmbracoRouteResult.Success: default: @@ -153,5 +161,28 @@ namespace Umbraco.Web.Common.Controllers break; } } + + private PublishedContentNotFoundResult GetNoTemplateResult(IPublishedRequest pcr) + { + // missing template, so we're in a 404 here + // so the content, if any, is a custom 404 page of some sort + if (!pcr.HasPublishedContent()) + { + // means the builder could not find a proper document to handle 404 + return new PublishedContentNotFoundResult(UmbracoContext); + } + else if (!pcr.HasTemplate()) + { + // means the engine could find a proper document, but the document has no template + // at that point there isn't much we can do + return new PublishedContentNotFoundResult( + UmbracoContext, + "In addition, no template exists to render the custom 404."); + } + else + { + return new PublishedContentNotFoundResult(UmbracoContext); + } + } } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index a94ee5e678..c4aae8839f 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -37,6 +37,8 @@ namespace Umbraco.Web.Website.DependencyInjection builder.Services.AddDataProtection(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.AddDistributedCache(); diff --git a/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs b/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs new file mode 100644 index 0000000000..d617dbbab4 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Web.Common.Controllers; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// Determines if a custom controller can hijack the current route + /// + public class HijackedRouteEvaluator + { + private readonly ILogger _logger; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private const string DefaultActionName = nameof(RenderController.Index); + + /// + /// Initializes a new instance of the class. + /// + public HijackedRouteEvaluator( + ILogger logger, + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + { + _logger = logger; + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + } + + public HijackedRouteResult Evaluate(string controller, string action) + { + IReadOnlyList candidates = FindControllerCandidates(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) + { + ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0]; + + // ensure the controller is of type IRenderController and ControllerBase + if (TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo) + && TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo)) + { + // now check if the custom action matches + var customActionExists = action != null && customControllerCandidates.Any(x => x.ActionName.InvariantEquals(action)); + + // it's a hijacked route with a custom controller, so return the the values + return new HijackedRouteResult( + true, + controllerDescriptor.ControllerName, + controllerDescriptor.ControllerTypeInfo, + customActionExists ? action : DefaultActionName); + } + 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(IRenderController).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 HijackedRouteResult.Failed(); + } + + /// + /// Return a list of controller candidates that match the custom controller and action names + /// + private IReadOnlyList FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName) + { + var descriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items + .Cast() + .Where(x => x.ControllerName.InvariantEquals(customControllerName) + && (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName)))) + .ToList(); + + return descriptors; + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs b/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs new file mode 100644 index 0000000000..f88bdfa2fd --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs @@ -0,0 +1,50 @@ +using System; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// The result from evaluating if a route can be hijacked + /// + public class HijackedRouteResult + { + /// + /// Returns a failed result + /// + public static HijackedRouteResult Failed() => new HijackedRouteResult(false, null, null, null); + + /// + /// Initializes a new instance of the class. + /// + public HijackedRouteResult( + bool success, + string controllerName, + Type controllerType, + string actionName) + { + Success = success; + ControllerName = controllerName; + ControllerType = controllerType; + ActionName = actionName; + } + + /// + /// Gets a value indicating if 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; } + } +} diff --git a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs new file mode 100644 index 0000000000..7af41d865b --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// Used to create + /// + public interface IUmbracoRouteValuesFactory + { + /// + /// Creates + /// + UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request); + } +} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 4916faeece..9cb920f434 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -1,22 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; -using Umbraco.Core.Strings; using Umbraco.Extensions; -using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Routing; using Umbraco.Web.Routing; using Umbraco.Web.Website.Controllers; @@ -37,13 +28,11 @@ namespace Umbraco.Web.Website.Routing { private readonly ILogger _logger; private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IUmbracoRenderingDefaults _renderingDefaults; - private readonly IShortStringHelper _shortStringHelper; - private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IPublishedRouter _publishedRouter; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IRuntimeState _runtime; + private readonly IUmbracoRouteValuesFactory _routeValuesFactory; /// /// Initializes a new instance of the class. @@ -51,23 +40,19 @@ namespace Umbraco.Web.Website.Routing public UmbracoRouteValueTransformer( ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoRenderingDefaults renderingDefaults, - IShortStringHelper shortStringHelper, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IPublishedRouter publishedRouter, IOptions globalSettings, IHostingEnvironment hostingEnvironment, - IRuntimeState runtime) + IRuntimeState runtime, + IUmbracoRouteValuesFactory routeValuesFactory) { _logger = logger; _umbracoContextAccessor = umbracoContextAccessor; - _renderingDefaults = renderingDefaults; - _shortStringHelper = shortStringHelper; - _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _publishedRouter = publishedRouter; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; _runtime = runtime; + _routeValuesFactory = routeValuesFactory; } /// @@ -104,7 +89,7 @@ namespace Umbraco.Web.Website.Routing IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext); - UmbracoRouteValues routeDef = GetUmbracoRouteDefinition(httpContext, values, publishedRequest); + UmbracoRouteValues routeDef = _routeValuesFactory.Create(httpContext, values, publishedRequest); values["controller"] = routeDef.ControllerName; if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) @@ -115,119 +100,6 @@ namespace Umbraco.Web.Website.Routing return await Task.FromResult(values); } - /// - /// Returns a object based on the current content request - /// - private UmbracoRouteValues GetUmbracoRouteDefinition(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) - { - if (httpContext is null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (values is null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - Type defaultControllerType = _renderingDefaults.DefaultControllerType; - var defaultControllerName = ControllerExtensions.GetControllerName(defaultControllerType); - - string customActionName = null; - - // check that a template is defined), if it doesn't and there is a hijacked route it will just route - // to the index Action - if (request.HasTemplate()) - { - // the template Alias should always be already saved with a safe name. - // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed - // with the action name attribute. - customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); - } - - // creates the default route definition which maps to the 'UmbracoController' controller - var def = new UmbracoRouteValues( - request, - defaultControllerName, - defaultControllerType, - templateName: customActionName); - - var customControllerName = request.PublishedContent?.ContentType.Alias; - if (customControllerName != null) - { - def = DetermineHijackedRoute(def, customControllerName, customActionName, request); - } - - // store the route definition - values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def); - - return def; - } - - private UmbracoRouteValues DetermineHijackedRoute(UmbracoRouteValues routeValues, string customControllerName, string customActionName, IPublishedRequest request) - { - IReadOnlyList candidates = FindControllerCandidates(customControllerName, customActionName, routeValues.ActionName); - - // check if there's a custom controller assigned, base on the document type alias. - var customControllerCandidates = candidates.Where(x => x.ControllerName.InvariantEquals(customControllerName)).ToList(); - - // check if that custom controller exists - if (customControllerCandidates.Count > 0) - { - ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0]; - - // ensure the controller is of type IRenderController and ControllerBase - if (TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo) - && TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo)) - { - // now check if the custom action matches - var customActionExists = customActionName != null && customControllerCandidates.Any(x => x.ActionName.InvariantEquals(customActionName)); - - // it's a hijacked route with a custom controller, so return the the values - return new UmbracoRouteValues( - request, - controllerDescriptor.ControllerName, - controllerDescriptor.ControllerTypeInfo, - customActionExists ? customActionName : routeValues.ActionName, - customActionName, - true); // Hijacked = true - } - 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}'.", - request.PublishedContent.ContentType.Alias, - controllerDescriptor.ControllerTypeInfo.FullName, - typeof(IRenderController).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 routeValues; - } - - /// - /// Return a list of controller candidates that match the custom controller and action names - /// - private IReadOnlyList FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName) - { - var descriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items - .Cast() - .Where(x => x.ControllerName.InvariantEquals(customControllerName) - && (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName)))) - .ToList(); - - return descriptors; - } - private async Task RouteRequestAsync(IUmbracoContext umbracoContext) { // ok, process @@ -240,20 +112,9 @@ namespace Umbraco.Web.Website.Routing // an immutable object. The only way to make this better would be to have a RouteRequest // as part of UmbracoContext but then it will require a PublishedRouter dependency so not sure that's worth it. // Maybe could be a one-time Set method instead? - return umbracoContext.PublishedRequest = await _publishedRouter.RouteRequestAsync(requestBuilder); + IPublishedRequest publishedRequest = umbracoContext.PublishedRequest = await _publishedRouter.RouteRequestAsync(requestBuilder); - // // HandleHttpResponseStatus returns a value indicating that the request should - // // not be processed any further, eg because it has been redirect. then, exit. - // if (UmbracoModule.HandleHttpResponseStatus(httpContext, request, _logger)) - // return; - // if (!request.HasPublishedContent == false) - // { - // // httpContext.RemapHandler(new PublishedContentNotFoundHandler()); - // } - // else - // { - // // RewriteToUmbracoHandler(httpContext, request); - // } + return publishedRequest; } } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs new file mode 100644 index 0000000000..b101cf155a --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -0,0 +1,135 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Features; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Web.Website.Routing +{ + + /// + /// Used to create + /// + public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory + { + private readonly ILogger _logger; + private readonly IUmbracoRenderingDefaults _renderingDefaults; + private readonly IShortStringHelper _shortStringHelper; + private readonly UmbracoFeatures _umbracoFeatures; + private readonly HijackedRouteEvaluator _hijackedRouteEvaluator; + private readonly Lazy _defaultControllerName; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRouteValuesFactory( + ILogger logger, + IUmbracoRenderingDefaults renderingDefaults, + IShortStringHelper shortStringHelper, + UmbracoFeatures umbracoFeatures, + HijackedRouteEvaluator hijackedRouteEvaluator) + { + _logger = logger; + _renderingDefaults = renderingDefaults; + _shortStringHelper = shortStringHelper; + _umbracoFeatures = umbracoFeatures; + _hijackedRouteEvaluator = hijackedRouteEvaluator; + _defaultControllerName = new Lazy(() => ControllerExtensions.GetControllerName(_renderingDefaults.DefaultControllerType)); + } + + /// + /// Gets the default controller name + /// + protected string DefaultControllerName => _defaultControllerName.Value; + + /// + public UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values is null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (request is null) + { + 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 + // to the index Action + if (request.HasTemplate()) + { + // the template Alias should always be already saved with a safe name. + // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed + // with the action name attribute. + customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); + } + + // creates the default route definition which maps to the 'UmbracoController' controller + var def = new UmbracoRouteValues( + request, + DefaultControllerName, + defaultControllerType, + templateName: customActionName); + + var customControllerName = request.PublishedContent?.ContentType.Alias; + if (customControllerName != null) + { + HijackedRouteResult hijackedResult = _hijackedRouteEvaluator.Evaluate(customControllerName, customActionName); + if (hijackedResult.Success) + { + def = new UmbracoRouteValues( + request, + hijackedResult.ControllerName, + hijackedResult.ControllerType, + hijackedResult.ActionName, + customActionName, + true); + } + } + + // Here we need to check if there is no hijacked route and no template assigned, + // if this is the case we want to return a blank page. + // We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, + // for example for json rendering in headless. + if (!request.HasTemplate() + && !_umbracoFeatures.Disabled.DisableTemplates + && !def.HasHijackedRoute) + { + // TODO: this is basically a 404, in v8 this will re-run the pipeline + // in order to see if a last chance finder finds some content + // At this point we're already done building the request and we have an immutable + // request. in v8 it was re-mutated :/ I really don't want to do that + + // In this case we'll render the NoTemplate action + def = new UmbracoRouteValues( + request, + DefaultControllerName, + defaultControllerType, + nameof(RenderController.NoTemplate)); + } + + // store the route definition + values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def); + + return def; + } + } +}