Prioritise surface controller requests highest (#13919)

* Fixed issue mentioned in https://github.com/umbraco/Umbraco-CMS/issues/13836

Now surface controller requests always has higher priority than other. So if this is both a surface controller request and a virtual page request, it will execute the surface controller.

* Moved some login into private methods

* More clean up

* Clean up

* Only handle valid candidates
This commit is contained in:
Bjarke Berg
2023-03-08 13:43:35 +01:00
committed by GitHub
parent a8a8a7fea5
commit de6e9aeb31
2 changed files with 119 additions and 0 deletions

View File

@@ -64,6 +64,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IUmbracoVirtualPageRoute, UmbracoVirtualPageRoute>();
builder.Services.AddSingleton<IUmbracoRouteValuesFactory, UmbracoRouteValuesFactory>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();
builder.Services.AddSingleton<MatcherPolicy, SurfaceControllerMatcherPolicy>();
builder.Services.AddSingleton<FrontEndRoutes>();

View File

@@ -0,0 +1,118 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Website.Routing;
/// <summary>
/// Ensures the surface controller requests takes priority over other things like virtual routes.
/// Also ensures that requests to a surface controller on a virtual route will return 405, like HttpMethodMatcherPolicy ensures for non-virtual route requests.
/// </summary>
internal class SurfaceControllerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
private const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
public override int Order { get; } // default order should be okay. Count be everything positive to not conflict with MS policies
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
// In theory all endpoints can have the query string data for a surface controller
return true;
}
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
ArgumentNullException.ThrowIfNull(nameof(httpContext));
ArgumentNullException.ThrowIfNull(nameof(candidates));
if (candidates.Count < 2)
{
return Task.CompletedTask;
}
int? surfaceControllerIndex = GetSurfaceControllerCandidateIndex(candidates);
if (surfaceControllerIndex.HasValue)
{
HashSet<string> allowedHttpMethods = GetAllowedHttpMethods(candidates[surfaceControllerIndex.Value]);
if (allowedHttpMethods.Any()
&& allowedHttpMethods.Contains(httpContext.Request.Method) is false)
{
// We need to handle this as a 405 like the HttpMethodMatcherPolicy would do.
httpContext.SetEndpoint(CreateRejectEndpoint(allowedHttpMethods));
httpContext.Request.RouteValues = null!;
}
else
{
// Otherwise we invalidate all other endpoints than the surface controller that matched.
InvalidateAllCandidatesExceptIndex(candidates, surfaceControllerIndex.Value);
}
}
return Task.CompletedTask;
}
private static HashSet<string> GetAllowedHttpMethods(CandidateState candidate)
{
var surfaceControllerAllowedHttpMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
IHttpMethodMetadata? httpMethodMetadata = candidate.Endpoint?.Metadata.GetMetadata<IHttpMethodMetadata>();
if (httpMethodMetadata is not null)
{
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
surfaceControllerAllowedHttpMethods.Add(httpMethod);
}
}
return surfaceControllerAllowedHttpMethods;
}
private static int? GetSurfaceControllerCandidateIndex(CandidateSet candidates)
{
for (var i = 0; i < candidates.Count; i++)
{
if (candidates.IsValidCandidate(i))
{
CandidateState candidate = candidates[i];
ControllerActionDescriptor? controllerActionDescriptor =
candidate.Endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
if (controllerActionDescriptor?.ControllerTypeInfo.IsType<SurfaceController>() == true)
{
return i;
}
}
}
return null;
}
private static void InvalidateAllCandidatesExceptIndex(CandidateSet candidates, int index)
{
for (var i = 0; i < candidates.Count; i++)
{
if (i != index)
{
candidates.SetValidity(i, false);
}
}
}
private static Endpoint CreateRejectEndpoint(ISet<string> allowedHttpMethods) =>
new Endpoint(
(context) =>
{
context.Response.StatusCode = 405;
context.Response.Headers.Allow = string.Join(", ", allowedHttpMethods);
return Task.CompletedTask;
},
EndpointMetadataCollection.Empty,
Http405EndpointDisplayName);
}