Files
Umbraco-CMS/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
Nikolaj Geisle 4f3d680f06 V10: Build warnings in Web.Website (#12332)
* add new rule to globalconfig

* Fix warnings in Web.Website

* Fix more warnings in Web.Website

* Fix more build warnings in Web.Website

* Fix more warnings in Web.Website

* Fix tests

* Fix proper constructor call

* Fix not being able to run project

* Fix Obsolete method

Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk>
2022-05-06 15:06:39 +02:00

287 lines
12 KiB
C#

using System.Net;
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;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Extensions;
using static Umbraco.Cms.Core.Constants.Web.Routing;
using RouteDirection = Umbraco.Cms.Core.Routing.RouteDirection;
namespace Umbraco.Cms.Web.Website.Routing;
/// <summary>
/// The route value transformer for Umbraco front-end routes
/// </summary>
/// <remarks>
/// NOTE: In aspnet 5 DynamicRouteValueTransformer has been improved, see
/// https://github.com/dotnet/aspnetcore/issues/21471
/// It seems as though with the "State" parameter we could more easily assign the IPublishedRequest or
/// IPublishedContent
/// or UmbracoContext more easily that way. In the meantime we will rely on assigning the IPublishedRequest to the
/// route values along with the IPublishedContent to the umbraco context
/// have created a GH discussion here https://github.com/dotnet/aspnetcore/discussions/28562 we'll see if anyone
/// responds
/// </remarks>
public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
{
private readonly IControllerActionSearcher _controllerActionSearcher;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly ILogger<UmbracoRouteValueTransformer> _logger;
private readonly IPublicAccessRequestHandler _publicAccessRequestHandler;
private readonly IPublishedRouter _publishedRouter;
private readonly IRoutableDocumentFilter _routableDocumentFilter;
private readonly IUmbracoRouteValuesFactory _routeValuesFactory;
private readonly IRuntimeState _runtime;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
[Obsolete("Please use constructor that does not take IOptions<GlobalSettings>, IHostingEnvironment & IEventAggregator instead")]
public UmbracoRouteValueTransformer(
ILogger<UmbracoRouteValueTransformer> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedRouter publishedRouter,
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtime,
IUmbracoRouteValuesFactory routeValuesFactory,
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IEventAggregator eventAggregator,
IPublicAccessRequestHandler publicAccessRequestHandler)
: this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer" /> class.
/// </summary>
public UmbracoRouteValueTransformer(
ILogger<UmbracoRouteValueTransformer> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedRouter publishedRouter,
IRuntimeState runtime,
IUmbracoRouteValuesFactory routeValuesFactory,
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IPublicAccessRequestHandler publicAccessRequestHandler)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_routeValuesFactory = routeValuesFactory ?? throw new ArgumentNullException(nameof(routeValuesFactory));
_routableDocumentFilter =
routableDocumentFilter ?? throw new ArgumentNullException(nameof(routableDocumentFilter));
_dataProtectionProvider = dataProtectionProvider;
_controllerActionSearcher = controllerActionSearcher;
_publicAccessRequestHandler = publicAccessRequestHandler;
}
/// <inheritdoc />
public override async ValueTask<RouteValueDictionary> TransformAsync(
HttpContext httpContext, RouteValueDictionary values)
{
// If we aren't running, then we have nothing to route
if (_runtime.Level != RuntimeLevel.Run)
{
return null!;
}
// will be null for any client side requests like JS, etc...
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
{
return null!;
}
if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path))
{
return null!;
}
// Don't execute if there are already UmbracoRouteValues assigned.
// This can occur if someone else is dynamically routing and in which case we don't want to overwrite
// the routing work being done there.
UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues != null)
{
return null!;
}
// Check if there is no existing content and return the no content controller
if (!umbracoContext.Content?.HasContent() ?? false)
{
return new RouteValueDictionary
{
[ControllerToken] = ControllerExtensions.GetControllerName<RenderNoContentController>(),
[ActionToken] = nameof(RenderNoContentController.Index),
};
}
IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext);
umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
// now we need to do some public access checks
umbracoRouteValues =
await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues);
// Store the route values as a httpcontext feature
httpContext.Features.Set(umbracoRouteValues);
// Need to check if there is form data being posted back to an Umbraco URL
PostedDataProxyInfo? postedInfo = GetFormInfo(httpContext, values);
if (postedInfo != null)
{
return HandlePostedValues(postedInfo, httpContext);
}
UmbracoRouteResult? routeResult = umbracoRouteValues?.PublishedRequest.GetRouteResult();
if (!routeResult.HasValue || routeResult == UmbracoRouteResult.NotFound)
{
// No content was found, not by any registered 404 handlers and
// not by the IContentLastChanceFinder. In this case we want to return
// our default 404 page but we cannot return route values now because
// it's possible that a developer is handling dynamic routes too.
// Our 404 page will be handled with the NotFoundSelectorPolicy
return null!;
}
// See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_
// We should apparenlty not be modified these values.
// So we create new ones.
var newValues = new RouteValueDictionary { [ControllerToken] = umbracoRouteValues?.ControllerName };
if (string.IsNullOrWhiteSpace(umbracoRouteValues?.ActionName) == false)
{
newValues[ActionToken] = umbracoRouteValues.ActionName;
}
return newValues;
}
private async Task<IPublishedRequest> RouteRequestAsync(IUmbracoContext umbracoContext)
{
// ok, process
// instantiate, prepare and process the published content request
// important to use CleanedUmbracoUrl - lowercase path-only version of the current url
IPublishedRequestBuilder requestBuilder =
await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl);
// TODO: This is ugly with the re-assignment to umbraco context but at least its now
// 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?
IPublishedRequest routedRequest =
await _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(RouteDirection.Inbound));
umbracoContext.PublishedRequest = routedRequest;
return routedRequest;
}
/// <summary>
/// Checks the request and query strings to see if it matches the definition of having a Surface controller
/// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information.
/// </summary>
private PostedDataProxyInfo? GetFormInfo(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
// if it is a POST/GET then a `ufprt` value must be in the request
var ufprt = httpContext.Request.GetUfprt();
if (string.IsNullOrWhiteSpace(ufprt))
{
return null;
}
if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(
_dataProtectionProvider,
ufprt,
out IDictionary<string, string?>? decodedUfprt))
{
return null;
}
// Get all route values that are not the default ones and add them separately so they eventually get to action parameters
foreach (KeyValuePair<string, string?> item in decodedUfprt.Where(x =>
ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false))
{
values[item.Key] = item.Value;
}
// return the proxy info without the surface id... could be a local controller.
return new PostedDataProxyInfo
{
ControllerName =
WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Controller).Value),
ActionName =
WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Action).Value),
Area = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Area).Value),
};
}
private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext)
{
// set the standard route values/tokens
var values = new RouteValueDictionary
{
[ControllerToken] = postedInfo.ControllerName, [ActionToken] = postedInfo.ActionName,
};
ControllerActionDescriptor? surfaceControllerDescriptor =
_controllerActionSearcher.Find<SurfaceController>(httpContext, postedInfo.ControllerName, postedInfo.ActionName, postedInfo.Area);
if (surfaceControllerDescriptor == null)
{
throw new InvalidOperationException(
"Could not find a Surface controller route in the RouteTable for controller name " +
postedInfo.ControllerName);
}
// set the area if one is there.
if (!postedInfo.Area.IsNullOrWhiteSpace())
{
values["area"] = postedInfo.Area;
}
return values;
}
private class PostedDataProxyInfo
{
public string? ControllerName { get; set; }
public string? ActionName { get; set; }
public string? Area { get; set; }
}
// Define reserved dictionary keys for controller, action and area specified in route additional values data
private static class ReservedAdditionalKeys
{
internal const string Controller = "c";
internal const string Action = "a";
internal const string Area = "ar";
internal static readonly string[] AllKeys = { Controller, Action, Area };
}
}