using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Features; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Website.Routing; /// /// Used to create /// public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory { private readonly IControllerActionSearcher _controllerActionSearcher; private readonly Lazy _defaultControllerDescriptor; private readonly Lazy _defaultControllerName; private readonly IPublishedRouter _publishedRouter; private readonly IShortStringHelper _shortStringHelper; private readonly UmbracoFeatures _umbracoFeatures; /// /// Initializes a new instance of the class. /// public UmbracoRouteValuesFactory( IOptions renderingDefaults, IShortStringHelper shortStringHelper, UmbracoFeatures umbracoFeatures, IControllerActionSearcher controllerActionSearcher, IPublishedRouter publishedRouter) { _shortStringHelper = shortStringHelper; _umbracoFeatures = umbracoFeatures; _controllerActionSearcher = controllerActionSearcher; _publishedRouter = publishedRouter; _defaultControllerName = new Lazy(() => ControllerExtensions.GetControllerName(renderingDefaults.Value.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; }); } /// /// Gets the default controller name /// protected string DefaultControllerName => _defaultControllerName.Value; /// public async Task CreateAsync(HttpContext httpContext, IPublishedRequest request) { if (httpContext is null) { throw new ArgumentNullException(nameof(httpContext)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } string? customActionName = GetTemplateName(request); // The default values for the default controller/action var def = new UmbracoRouteValues( request, _defaultControllerDescriptor.Value, customActionName); def = CheckHijackedRoute(httpContext, def, out var hasHijackedRoute); def = await CheckNoTemplateAsync(httpContext, def, hasHijackedRoute); return def; } /// /// Check if the route is hijacked and return new route values /// private UmbracoRouteValues CheckHijackedRoute( HttpContext httpContext, UmbracoRouteValues def, out bool hasHijackedRoute) { IPublishedRequest request = def.PublishedRequest; var customControllerName = request.PublishedContent?.ContentType?.Alias; if (customControllerName != null) { ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find(httpContext, customControllerName, def.TemplateName); if (descriptor != null) { hasHijackedRoute = true; return new UmbracoRouteValues( request, descriptor, def.TemplateName); } } hasHijackedRoute = false; return def; } /// /// 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 async Task CheckNoTemplateAsync( HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) { IPublishedRequest request = def.PublishedRequest; // Here we need to check if there is no hijacked route and no template assigned but there is a content item. // If this is the case we want to return a blank page, the only exception being if the content item has a redirect field present. // 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.HasPublishedContent() && !request.HasTemplate() && !request.IsRedirect() && !_umbracoFeatures.Disabled.DisableTemplates && !hasHijackedRoute) { IPublishedContent? content = request.PublishedContent; // This is basically a 404 even if there is content found. // We then need to re-run this through the pipeline for the last // chance finders to work. // Set to null since we are telling it there is no content. request = await _publishedRouter.UpdateRequestAsync(request, null); if (request == null) { throw new InvalidOperationException( $"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null"); } string? customActionName = GetTemplateName(request); def = new UmbracoRouteValues( request, def.ControllerActionDescriptor, customActionName); // if the content has changed, we must then again check for hijacked routes if (content != request.PublishedContent) { def = CheckHijackedRoute(httpContext, def, out _); } } return def; } private string? GetTemplateName(IPublishedRequest request) { // 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. return request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); } return null; } }