diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs index 1f76f6f802..569f38139d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs @@ -74,6 +74,7 @@ public sealed class RichTextEditorPastedImages // we have already processed to avoid dupes var uploadedImages = new Dictionary(); + foreach (HtmlNode? img in tmpImages) { // The data attribute contains the path to the tmp img to persist as a media item @@ -84,6 +85,11 @@ public sealed class RichTextEditorPastedImages continue; } + if (IsValidPath(tmpImgPath) == false) + { + continue; + } + var absoluteTempImagePath = _hostingEnvironment.MapPathContentRoot(tmpImgPath); var fileName = Path.GetFileName(absoluteTempImagePath); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); @@ -184,4 +190,6 @@ public sealed class RichTextEditorPastedImages return htmlDoc.DocumentNode.OuterHtml; } + + private bool IsValidPath(string imagePath) => imagePath.StartsWith(Constants.SystemDirectories.TempImageUploads); } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 12c47c5fa4..dc98c5b813 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -64,6 +64,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.Website/Routing/SurfaceControllerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/SurfaceControllerMatcherPolicy.cs new file mode 100644 index 0000000000..f2a834ca09 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/SurfaceControllerMatcherPolicy.cs @@ -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; + +/// +/// 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. +/// +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 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 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 GetAllowedHttpMethods(CandidateState candidate) + { + var surfaceControllerAllowedHttpMethods = new HashSet(StringComparer.OrdinalIgnoreCase); + + IHttpMethodMetadata? httpMethodMetadata = candidate.Endpoint?.Metadata.GetMetadata(); + 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(); + + if (controllerActionDescriptor?.ControllerTypeInfo.IsType() == 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 allowedHttpMethods) => + new Endpoint( + (context) => + { + context.Response.StatusCode = 405; + + context.Response.Headers.Allow = string.Join(", ", allowedHttpMethods); + + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + Http405EndpointDisplayName); +} diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index d50f95a907..0989706f68 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -26,6 +26,8 @@ false false + +