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;
}
}