diff --git a/src/Umbraco.Web.Common/ActionsResults/PublishedContentNotFoundResult.cs b/src/Umbraco.Web.Common/ActionsResults/PublishedContentNotFoundResult.cs index 417ed622bd..6654c17981 100644 --- a/src/Umbraco.Web.Common/ActionsResults/PublishedContentNotFoundResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/PublishedContentNotFoundResult.cs @@ -1,57 +1,54 @@ using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Web.Common.ActionsResults +namespace Umbraco.Cms.Web.Common.ActionsResults; + +/// +/// Returns the Umbraco not found result +/// +public class PublishedContentNotFoundResult : IActionResult { + private readonly string? _message; + private readonly IUmbracoContext _umbracoContext; + /// - /// Returns the Umbraco not found result + /// Initializes a new instance of the class. /// - public class PublishedContentNotFoundResult : IActionResult + public PublishedContentNotFoundResult(IUmbracoContext umbracoContext, string? message = null) { - private readonly IUmbracoContext _umbracoContext; - private readonly string? _message; + _umbracoContext = umbracoContext; + _message = message; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedContentNotFoundResult(IUmbracoContext umbracoContext, string? message = null) + /// + public async Task ExecuteResultAsync(ActionContext context) + { + HttpResponse response = context.HttpContext.Response; + + response.Clear(); + + response.StatusCode = StatusCodes.Status404NotFound; + + IPublishedRequest? frequest = _umbracoContext.PublishedRequest; + var reason = "Cannot render the page at URL '{0}'."; + if (frequest?.HasPublishedContent() == false) { - _umbracoContext = umbracoContext; - _message = message; + reason = "No umbraco document matches the URL '{0}'."; + } + else if (frequest?.HasTemplate() == false) + { + reason = "No template exists to render the document at URL '{0}'."; } - /// - public async Task ExecuteResultAsync(ActionContext context) - { - HttpResponse response = context.HttpContext.Response; + var viewResult = new ViewResult { ViewName = "~/umbraco/UmbracoWebsite/NotFound.cshtml" }; + context.HttpContext.Items.Add( + "reason", + string.Format(reason, WebUtility.HtmlEncode(_umbracoContext.OriginalRequestUrl.PathAndQuery))); + context.HttpContext.Items.Add("message", _message); - response.Clear(); - - response.StatusCode = StatusCodes.Status404NotFound; - - IPublishedRequest? frequest = _umbracoContext.PublishedRequest; - var reason = "Cannot render the page at URL '{0}'."; - if (frequest?.HasPublishedContent() == false) - { - reason = "No umbraco document matches the URL '{0}'."; - } - else if (frequest?.HasTemplate() == false) - { - reason = "No template exists to render the document at URL '{0}'."; - } - - var viewResult = new ViewResult - { - ViewName = "~/umbraco/UmbracoWebsite/NotFound.cshtml" - }; - context.HttpContext.Items.Add("reason", string.Format(reason, WebUtility.HtmlEncode(_umbracoContext.OriginalRequestUrl.PathAndQuery))); - context.HttpContext.Items.Add("message", _message); - - await viewResult.ExecuteResultAsync(context); - } + await viewResult.ExecuteResultAsync(context); } } diff --git a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs index 92f5d7aed7..8aa5188c3f 100644 --- a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs @@ -1,14 +1,11 @@ using System.Net; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Cms.Web.Common.ActionsResults +namespace Umbraco.Cms.Web.Common.ActionsResults; + +// TODO: What is the purpose of this? Doesn't seem to add any benefit +public class UmbracoProblemResult : ObjectResult { - // TODO: What is the purpose of this? Doesn't seem to add any benefit - public class UmbracoProblemResult : ObjectResult - { - public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) : base(new {Message = message}) - { - StatusCode = (int) httpStatusCode; - } - } + public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) + : base(new { Message = message }) => StatusCode = (int)httpStatusCode; } diff --git a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs index 3207272b66..d6996f6355 100644 --- a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs @@ -5,64 +5,61 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.ActionsResults +namespace Umbraco.Cms.Web.Common.ActionsResults; + +// TODO: This should probably follow the same conventions as in aspnet core and use ProblemDetails +// and ProblemDetails factory. See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1977 +// ProblemDetails is explicitly checked for in the application model. +// In our base class UmbracoAuthorizedApiController the logic is there to create a ProblemDetails. +// However, to do this will require changing how angular deals with errors since the response will +// probably be different. Would just be better to follow the aspnet patterns. + +/// +/// Custom result to return a validation error message with required headers +/// +/// +/// The default status code is a 400 http response +/// +public class ValidationErrorResult : ObjectResult { - // TODO: This should probably follow the same conventions as in aspnet core and use ProblemDetails - // and ProblemDetails factory. See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1977 - // ProblemDetails is explicitly checked for in the application model. - // In our base class UmbracoAuthorizedApiController the logic is there to create a ProblemDetails. - // However, to do this will require changing how angular deals with errors since the response will - // probably be different. Would just be better to follow the aspnet patterns. + public ValidationErrorResult(ModelStateDictionary modelState) + : this(new SimpleValidationModel(modelState.ToErrorDictionary())) + { + } + + public ValidationErrorResult(object? value, int statusCode) + : base(value) => StatusCode = statusCode; + + public ValidationErrorResult(object? value) + : this(value, StatusCodes.Status400BadRequest) + { + } + + // TODO: Like here, shouldn't we use ProblemDetails? + public ValidationErrorResult(string errorMessage, int statusCode) + : base(new { Message = errorMessage }) => + StatusCode = statusCode; + + public ValidationErrorResult(string errorMessage) + : this(errorMessage, StatusCodes.Status400BadRequest) + { + } /// - /// Custom result to return a validation error message with required headers + /// Typically this should not be used and just use the ValidationProblem method on the base controller class. /// - /// - /// The default status code is a 400 http response - /// - public class ValidationErrorResult : ObjectResult + /// + /// + public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage) { - /// - /// Typically this should not be used and just use the ValidationProblem method on the base controller class. - /// - /// - /// - public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage) - { - var notificationModel = new SimpleNotificationModel - { - Message = errorMessage - }; - notificationModel.AddErrorNotification(errorMessage, string.Empty); - return new ValidationErrorResult(notificationModel); - } + var notificationModel = new SimpleNotificationModel { Message = errorMessage }; + notificationModel.AddErrorNotification(errorMessage, string.Empty); + return new ValidationErrorResult(notificationModel); + } - public ValidationErrorResult(ModelStateDictionary modelState) - : this(new SimpleValidationModel(modelState.ToErrorDictionary())) { } - - public ValidationErrorResult(object? value, int statusCode) : base(value) - { - StatusCode = statusCode; - } - - public ValidationErrorResult(object? value) : this(value, StatusCodes.Status400BadRequest) - { - } - - // TODO: Like here, shouldn't we use ProblemDetails? - public ValidationErrorResult(string errorMessage, int statusCode) : base(new { Message = errorMessage }) - { - StatusCode = statusCode; - } - - public ValidationErrorResult(string errorMessage) : this(errorMessage, StatusCodes.Status400BadRequest) - { - } - - public override void OnFormatting(ActionContext context) - { - base.OnFormatting(context); - context.HttpContext.Response.Headers["X-Status-Reason"] = "Validation failed"; - } + public override void OnFormatting(ActionContext context) + { + base.OnFormatting(context); + context.HttpContext.Response.Headers["X-Status-Reason"] = "Validation failed"; } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs index 2c817d9eb8..02d8f886cc 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -1,21 +1,18 @@ -using System; +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +public interface IUmbracoApplicationBuilder { - public interface IUmbracoApplicationBuilder - { - /// - /// EXPERT call to replace the middlewares that Umbraco installs by default with a completely custom pipeline. - /// - /// - /// - IUmbracoEndpointBuilder WithCustomMiddleware(Action configureUmbracoMiddleware); + /// + /// EXPERT call to replace the middlewares that Umbraco installs by default with a completely custom pipeline. + /// + /// + /// + IUmbracoEndpointBuilder WithCustomMiddleware(Action configureUmbracoMiddleware); - /// - /// Called to include default middleware to run umbraco. - /// - /// - /// - IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware); - } + /// + /// Called to include default middleware to run umbraco. + /// + /// + /// + IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs index ecf62af32e..749c3feae4 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs @@ -1,33 +1,30 @@ -using System; +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +/// +/// The context object used during +/// +public interface IUmbracoApplicationBuilderContext : IUmbracoApplicationBuilderServices { /// - /// The context object used during + /// Called to include the core umbraco middleware. /// - public interface IUmbracoApplicationBuilderContext : IUmbracoApplicationBuilderServices - { - /// - /// Called to include the core umbraco middleware. - /// - void UseUmbracoCoreMiddleware(); + void UseUmbracoCoreMiddleware(); - /// - /// Manually runs the pre pipeline filters - /// - void RunPrePipeline(); + /// + /// Manually runs the pre pipeline filters + /// + void RunPrePipeline(); - /// - /// Manually runs the post pipeline filters - /// - void RunPostPipeline(); + /// + /// Manually runs the post pipeline filters + /// + void RunPostPipeline(); - /// - /// Called to include all of the default umbraco required middleware. - /// - /// - /// If using this method, there is no need to use - /// - void RegisterDefaultRequiredMiddleware(); - } + /// + /// Called to include all of the default umbraco required middleware. + /// + /// + /// If using this method, there is no need to use + /// + void RegisterDefaultRequiredMiddleware(); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs index 5310018969..c0c76906c8 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs @@ -1,16 +1,16 @@ -using System; using Microsoft.AspNetCore.Builder; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; + +/// +/// Services used during the Umbraco building phase. +/// +public interface IUmbracoApplicationBuilderServices { - /// - /// Services used during the Umbraco building phase. - /// - public interface IUmbracoApplicationBuilderServices - { - IApplicationBuilder AppBuilder { get; } - IServiceProvider ApplicationServices { get; } - IRuntimeState RuntimeState { get; } - } + IApplicationBuilder AppBuilder { get; } + + IServiceProvider ApplicationServices { get; } + + IRuntimeState RuntimeState { get; } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs index 58e0b8fec2..55b1c705bf 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs @@ -1,13 +1,10 @@ -using System; +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +public interface IUmbracoEndpointBuilder { - public interface IUmbracoEndpointBuilder - { - /// - /// Final call during app building to configure endpoints - /// - /// - void WithEndpoints(Action configureUmbraco); - } + /// + /// Final call during app building to configure endpoints + /// + /// + void WithEndpoints(Action configureUmbraco); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs index 6122c9ef2e..db8e8a44b1 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs @@ -1,13 +1,11 @@ using Microsoft.AspNetCore.Routing; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder -{ +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; - /// - /// A builder to allow encapsulating the enabled routing features in Umbraco - /// - public interface IUmbracoEndpointBuilderContext : IUmbracoApplicationBuilderServices - { - IEndpointRouteBuilder EndpointRouteBuilder { get; } - } +/// +/// A builder to allow encapsulating the enabled routing features in Umbraco +/// +public interface IUmbracoEndpointBuilderContext : IUmbracoApplicationBuilderServices +{ + IEndpointRouteBuilder EndpointRouteBuilder { get; } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs index d1cf866da1..1f86dbeed7 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs @@ -1,39 +1,39 @@ using Microsoft.AspNetCore.Builder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; + +/// +/// Used to modify the pipeline before and after Umbraco registers it's core +/// middlewares. +/// +/// +/// Mainly used for package developers. +/// +public interface IUmbracoPipelineFilter { /// - /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. + /// The name of the filter /// /// - /// Mainly used for package developers. + /// This can be used by developers to see what is registered and if anything should be re-ordered, removed, etc... /// - public interface IUmbracoPipelineFilter - { - /// - /// The name of the filter - /// - /// - /// This can be used by developers to see what is registered and if anything should be re-ordered, removed, etc... - /// - string Name { get; } + string Name { get; } - /// - /// Executes before Umbraco middlewares are registered - /// - /// - void OnPrePipeline(IApplicationBuilder app); + /// + /// Executes before Umbraco middlewares are registered + /// + /// + void OnPrePipeline(IApplicationBuilder app); - /// - /// Executes after core Umbraco middlewares are registered and before any Endpoints are declared - /// - /// - void OnPostPipeline(IApplicationBuilder app); + /// + /// Executes after core Umbraco middlewares are registered and before any Endpoints are declared + /// + /// + void OnPostPipeline(IApplicationBuilder app); - /// - /// Executes after just before any Umbraco endpoints are declared. - /// - /// - void OnEndpoints(IApplicationBuilder app); - } + /// + /// Executes after just before any Umbraco endpoints are declared. + /// + /// + void OnEndpoints(IApplicationBuilder app); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index 51af2ca22d..8bf36264eb 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,4 +1,3 @@ -using System; using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -12,160 +11,163 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; + +/// +/// A builder used to enable middleware and endpoints required for Umbraco to operate. +/// +/// +/// This helps to ensure that everything is registered in the correct order. +/// +public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEndpointBuilder, + IUmbracoApplicationBuilderContext { - /// - /// A builder used to enable middleware and endpoints required for Umbraco to operate. - /// - /// - /// This helps to ensure that everything is registered in the correct order. - /// - public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEndpointBuilder, IUmbracoApplicationBuilderContext + private readonly IOptions _umbracoPipelineStartupOptions; + + public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) { - private readonly IOptions _umbracoPipelineStartupOptions; + AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); + ApplicationServices = appBuilder.ApplicationServices; + RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); + _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); + } - public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) + public IServiceProvider ApplicationServices { get; } + + public IRuntimeState RuntimeState { get; } + + public IApplicationBuilder AppBuilder { get; } + + /// + public IUmbracoEndpointBuilder WithCustomMiddleware( + Action configureUmbracoMiddleware) + { + if (configureUmbracoMiddleware is null) { - AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); - ApplicationServices = appBuilder.ApplicationServices; - RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); - _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); + throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); } - public IServiceProvider ApplicationServices { get; } + configureUmbracoMiddleware(this); - public IRuntimeState RuntimeState { get; } + return this; + } - public IApplicationBuilder AppBuilder { get; } - - /// - public IUmbracoEndpointBuilder WithCustomMiddleware(Action configureUmbracoMiddleware) + /// + public IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware) + { + if (configureUmbracoMiddleware is null) { - if (configureUmbracoMiddleware is null) + throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); + } + + RunPrePipeline(); + + RegisterDefaultRequiredMiddleware(); + + RunPostPipeline(); + + configureUmbracoMiddleware(this); + + return this; + } + + /// + /// Registers the default required middleware to run Umbraco. + /// + public void RegisterDefaultRequiredMiddleware() + { + UseUmbracoCoreMiddleware(); + + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + AppBuilder.UseImageSharp(); + + // Get media file provider and request path/URL + MediaFileManager mediaFileManager = AppBuilder.ApplicationServices.GetRequiredService(); + if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider? mediaFileProvider)) + { + GlobalSettings globalSettings = + AppBuilder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment? hostingEnvironment = AppBuilder.ApplicationServices.GetService(); + var mediaRequestPath = hostingEnvironment?.ToAbsolute(globalSettings.UmbracoMediaPath); + + // Configure custom file provider for media + IWebHostEnvironment? webHostEnvironment = AppBuilder.ApplicationServices.GetService(); + if (webHostEnvironment is not null) { - throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); - } - - configureUmbracoMiddleware(this); - - return this; - } - - /// - public IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware) - { - if (configureUmbracoMiddleware is null) - { - throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); - } - - RunPrePipeline(); - - RegisterDefaultRequiredMiddleware(); - - RunPostPipeline(); - - configureUmbracoMiddleware(this); - - return this; - } - - /// - public void WithEndpoints(Action configureUmbraco) - { - IOptions startupOptions = ApplicationServices.GetRequiredService>(); - RunPreEndpointsPipeline(); - - AppBuilder.UseEndpoints(endpoints => - { - var umbAppBuilder = (IUmbracoEndpointBuilderContext)ActivatorUtilities.CreateInstance( - ApplicationServices, - new object[] { AppBuilder, endpoints }); - configureUmbraco(umbAppBuilder); - }); - } - - /// - /// Registers the default required middleware to run Umbraco. - /// - public void RegisterDefaultRequiredMiddleware() - { - UseUmbracoCoreMiddleware(); - - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - AppBuilder.UseImageSharp(); - - // Get media file provider and request path/URL - var mediaFileManager = AppBuilder.ApplicationServices.GetRequiredService(); - if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider? mediaFileProvider)) - { - GlobalSettings globalSettings = AppBuilder.ApplicationServices.GetRequiredService>().Value; - IHostingEnvironment? hostingEnvironment = AppBuilder.ApplicationServices.GetService(); - string? mediaRequestPath = hostingEnvironment?.ToAbsolute(globalSettings.UmbracoMediaPath); - - // Configure custom file provider for media - IWebHostEnvironment? webHostEnvironment = AppBuilder.ApplicationServices.GetService(); - if (webHostEnvironment is not null) - { - webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); - } - } - - AppBuilder.UseStaticFiles(); - - AppBuilder.UseUmbracoPluginsStaticFiles(); - - // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one - // will execute after endpoint routing. The ordering of everything is quite important here, see - // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 - // where we need to have UseAuthentication and UseAuthorization proceeding this call but before - // endpoints are defined. - AppBuilder.UseRouting(); - AppBuilder.UseAuthentication(); - AppBuilder.UseAuthorization(); - - // This must come after auth because the culture is based on the auth'd user - AppBuilder.UseRequestLocalization(); - - // Must be called after UseRouting and before UseEndpoints - AppBuilder.UseSession(); - - // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, - // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder - } - - public void UseUmbracoCoreMiddleware() - { - AppBuilder.UseUmbracoCore(); - AppBuilder.UseUmbracoRequestLogging(); - - // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed - // before endpoint routing middleware. - AppBuilder.UseUmbracoRouting(); - } - - public void RunPrePipeline() - { - foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) - { - filter.OnPrePipeline(AppBuilder); + webHostEnvironment.WebRootFileProvider = + webHostEnvironment.WebRootFileProvider.ConcatComposite( + new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); } } - public void RunPostPipeline() - { - foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) - { - filter.OnPostPipeline(AppBuilder); - } - } + AppBuilder.UseStaticFiles(); - private void RunPreEndpointsPipeline() + AppBuilder.UseUmbracoPluginsStaticFiles(); + + // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one + // will execute after endpoint routing. The ordering of everything is quite important here, see + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 + // where we need to have UseAuthentication and UseAuthorization proceeding this call but before + // endpoints are defined. + AppBuilder.UseRouting(); + AppBuilder.UseAuthentication(); + AppBuilder.UseAuthorization(); + + // This must come after auth because the culture is based on the auth'd user + AppBuilder.UseRequestLocalization(); + + // Must be called after UseRouting and before UseEndpoints + AppBuilder.UseSession(); + + // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, + // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder + } + + public void UseUmbracoCoreMiddleware() + { + AppBuilder.UseUmbracoCore(); + AppBuilder.UseUmbracoRequestLogging(); + + // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed + // before endpoint routing middleware. + AppBuilder.UseUmbracoRouting(); + } + + public void RunPrePipeline() + { + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) { - foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) - { - filter.OnEndpoints(AppBuilder); - } + filter.OnPrePipeline(AppBuilder); + } + } + + public void RunPostPipeline() + { + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) + { + filter.OnPostPipeline(AppBuilder); + } + } + + /// + public void WithEndpoints(Action configureUmbraco) + { + RunPreEndpointsPipeline(); + + AppBuilder.UseEndpoints(endpoints => + { + var umbAppBuilder = + (IUmbracoEndpointBuilderContext)ActivatorUtilities.CreateInstance( + ApplicationServices, AppBuilder, endpoints); + configureUmbraco(umbAppBuilder); + }); + } + + private void RunPreEndpointsPipeline() + { + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) + { + filter.OnEndpoints(AppBuilder); } } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs index 86e8f3e957..fa75ed116d 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs @@ -1,26 +1,27 @@ -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder -{ - /// - /// A builder to allow encapsulating the enabled endpoints in Umbraco - /// - internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilderContext - { - public UmbracoEndpointBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) - { - ApplicationServices = services; - EndpointRouteBuilder = endpointRouteBuilder; - RuntimeState = runtimeState; - AppBuilder = appBuilder; - } +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; - public IServiceProvider ApplicationServices { get; } - public IEndpointRouteBuilder EndpointRouteBuilder { get; } - public IRuntimeState RuntimeState { get; } - public IApplicationBuilder AppBuilder { get; } +/// +/// A builder to allow encapsulating the enabled endpoints in Umbraco +/// +internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilderContext +{ + public UmbracoEndpointBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) + { + ApplicationServices = services; + EndpointRouteBuilder = endpointRouteBuilder; + RuntimeState = runtimeState; + AppBuilder = appBuilder; } + + public IServiceProvider ApplicationServices { get; } + + public IEndpointRouteBuilder EndpointRouteBuilder { get; } + + public IRuntimeState RuntimeState { get; } + + public IApplicationBuilder AppBuilder { get; } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs index 25d006634a..aa11bc6bd9 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs @@ -1,37 +1,44 @@ -using System; using Microsoft.AspNetCore.Builder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; + +/// +/// Used to modify the pipeline before and after Umbraco registers it's core +/// middlewares. +/// +/// +/// Mainly used for package developers. +/// +public class UmbracoPipelineFilter : IUmbracoPipelineFilter { - /// - /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. - /// - /// - /// Mainly used for package developers. - /// - public class UmbracoPipelineFilter : IUmbracoPipelineFilter + public UmbracoPipelineFilter(string name) + : this(name, null, null, null) { - public UmbracoPipelineFilter(string name) : this(name, null, null, null) { } - - public UmbracoPipelineFilter( - string name, - Action? prePipeline, - Action? postPipeline, - Action? endpointCallback) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - PrePipeline = prePipeline; - PostPipeline = postPipeline; - Endpoints = endpointCallback; - } - - public Action? PrePipeline { get; set; } - public Action? PostPipeline { get; set; } - public Action? Endpoints { get; set; } - public string Name { get; } - - public void OnPrePipeline(IApplicationBuilder app) => PrePipeline?.Invoke(app); - public void OnPostPipeline(IApplicationBuilder app) => PostPipeline?.Invoke(app); - public void OnEndpoints(IApplicationBuilder app) => Endpoints?.Invoke(app); } + + public UmbracoPipelineFilter( + string name, + Action? prePipeline, + Action? postPipeline, + Action? endpointCallback) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + PrePipeline = prePipeline; + PostPipeline = postPipeline; + Endpoints = endpointCallback; + } + + public Action? PrePipeline { get; set; } + + public Action? PostPipeline { get; set; } + + public Action? Endpoints { get; set; } + + public string Name { get; } + + public void OnPrePipeline(IApplicationBuilder app) => PrePipeline?.Invoke(app); + + public void OnPostPipeline(IApplicationBuilder app) => PostPipeline?.Invoke(app); + + public void OnEndpoints(IApplicationBuilder app) => Endpoints?.Invoke(app); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs index 8cf2b4144a..48c5cd9408 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs @@ -1,22 +1,21 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Builder; -namespace Umbraco.Cms.Web.Common.ApplicationBuilder +namespace Umbraco.Cms.Web.Common.ApplicationBuilder; + +/// +/// Options to allow modifying the pipeline before and after Umbraco registers it's +/// core middlewares. +/// +public class UmbracoPipelineOptions { /// - /// Options to allow modifying the pipeline before and after Umbraco registers it's core middlewares. + /// Returns a mutable list of all registered startup filters /// - public class UmbracoPipelineOptions - { - /// - /// Returns a mutable list of all registered startup filters - /// - public IList PipelineFilters { get; } = new List(); + public IList PipelineFilters { get; } = new List(); - /// - /// Adds a filter to the list - /// - /// - public void AddFilter(IUmbracoPipelineFilter filter) => PipelineFilters.Add(filter); - } + /// + /// Adds a filter to the list + /// + /// + public void AddFilter(IUmbracoPipelineFilter filter) => PipelineFilters.Add(filter); } diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs index 146edb19e9..a4b926ac93 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs @@ -1,56 +1,51 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Cms.Web.Common.Attributes; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +// TODO: This should just exist in the back office project + +/// +/// An application model provider for all Umbraco Back Office controllers +/// +public class BackOfficeApplicationModelProvider : IApplicationModelProvider { - - // TODO: This should just exist in the back office project - - /// - /// An application model provider for all Umbraco Back Office controllers - /// - public class BackOfficeApplicationModelProvider : IApplicationModelProvider + private readonly List _actionModelConventions = new() { - private readonly List _actionModelConventions = new List() - { - new BackOfficeIdentityCultureConvention() - }; + new BackOfficeIdentityCultureConvention(), + }; - /// - /// - /// Will execute after - /// - public int Order => 0; + /// + /// + /// Will execute after + /// + public int Order => 0; - /// - public void OnProvidersExecuted(ApplicationModelProviderContext context) - { - } + /// + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } - /// - public void OnProvidersExecuting(ApplicationModelProviderContext context) + /// + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (ControllerModel controller in context.Result.Controllers) { - foreach (ControllerModel controller in context.Result.Controllers) + if (!IsBackOfficeController(controller)) { - if (!IsBackOfficeController(controller)) - { - continue; - } + continue; + } - foreach (ActionModel action in controller.Actions) + foreach (ActionModel action in controller.Actions) + { + foreach (IActionModelConvention convention in _actionModelConventions) { - foreach (IActionModelConvention convention in _actionModelConventions) - { - convention.Apply(action); - } + convention.Apply(action); } } } - - private bool IsBackOfficeController(ControllerModel controller) - => controller.Attributes.OfType().Any(); } + + private bool IsBackOfficeController(ControllerModel controller) + => controller.Attributes.OfType().Any(); } diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs index 8414662816..ffc2d33278 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Umbraco.Cms.Web.Common.Filters; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +// TODO: This should just exist in the back office project +public class BackOfficeIdentityCultureConvention : IActionModelConvention { - - // TODO: This should just exist in the back office project - - public class BackOfficeIdentityCultureConvention : IActionModelConvention - { - /// - public void Apply(ActionModel action) => action.Filters.Add(new BackOfficeCultureFilter()); - } + /// + public void Apply(ActionModel action) => action.Filters.Add(new BackOfficeCultureFilter()); } diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs index df0584386b..d2165a63ee 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs @@ -1,90 +1,86 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Cms.Web.Common.Attributes; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +/// +/// An application model provider for Umbraco API controllers to behave like WebApi controllers +/// +/// +/// +/// Conventions will be applied to controllers attributed with +/// +/// +/// This is nearly a copy of aspnetcore's ApiBehaviorApplicationModelProvider which supplies a convention for the +/// [ApiController] attribute, however that convention is too strict for our purposes so we will have our own. +/// Uses UmbracoJsonModelBinder for complex parameters and those with BindingSource of Body, but leaves the rest +/// alone see GH #11554 +/// +/// +/// See https://shazwazza.com/post/custom-body-model-binding-per-controller-in-asp-net-core/ +/// and https://github.com/dotnet/aspnetcore/issues/21724 +/// +/// +public class UmbracoApiBehaviorApplicationModelProvider : IApplicationModelProvider { + private readonly List _actionModelConventions; /// - /// An application model provider for Umbraco API controllers to behave like WebApi controllers + /// Initializes a new instance of the class. /// - /// - /// - /// Conventions will be applied to controllers attributed with - /// - /// - /// This is nearly a copy of aspnetcore's ApiBehaviorApplicationModelProvider which supplies a convention for the - /// [ApiController] attribute, however that convention is too strict for our purposes so we will have our own. - /// Uses UmbracoJsonModelBinder for complex parameters and those with BindingSource of Body, but leaves the rest alone see GH #11554 - /// - /// - /// See https://shazwazza.com/post/custom-body-model-binding-per-controller-in-asp-net-core/ - /// and https://github.com/dotnet/aspnetcore/issues/21724 - /// - /// - public class UmbracoApiBehaviorApplicationModelProvider : IApplicationModelProvider + public UmbracoApiBehaviorApplicationModelProvider(IModelMetadataProvider modelMetadataProvider) { - private readonly List _actionModelConventions; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoApiBehaviorApplicationModelProvider(IModelMetadataProvider modelMetadataProvider) + // see see https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#apicontroller-attribute + // for what these things actually do + // NOTE: we don't have attribute routing requirements and we cannot use ApiVisibilityConvention without attribute routing + _actionModelConventions = new List { - // see see https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#apicontroller-attribute - // for what these things actually do - // NOTE: we don't have attribute routing requirements and we cannot use ApiVisibilityConvention without attribute routing + new ClientErrorResultFilterConvention(), // Ensures the responses without any body is converted into a simple json object with info instead of a string like "Status Code: 404; Not Found" + new ConsumesConstraintForFormFileParameterConvention(), // If an controller accepts files, it must accept multipart/form-data. - _actionModelConventions = new List() + // This ensures that all parameters of type BindingSource.Body and those of complex type are bound + // using our own UmbracoJsonModelBinder + new UmbracoJsonModelBinderConvention(modelMetadataProvider), + }; + + Type defaultErrorType = typeof(ProblemDetails); + var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType); + _actionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute)); + } + + /// + /// + /// Will execute after + /// + public int Order => 0; + + /// + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } + + /// + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (ControllerModel controller in context.Result.Controllers) + { + if (!IsUmbracoApiController(controller)) { - new ClientErrorResultFilterConvention(), // Ensures the responses without any body is converted into a simple json object with info instead of a string like "Status Code: 404; Not Found" - new ConsumesConstraintForFormFileParameterConvention(), // If an controller accepts files, it must accept multipart/form-data. + continue; + } - // This ensures that all parameters of type BindingSource.Body and those of complex type are bound - // using our own UmbracoJsonModelBinder - new UmbracoJsonModelBinderConvention(modelMetadataProvider) - }; - - var defaultErrorType = typeof(ProblemDetails); - var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType); - _actionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute)); - } - - /// - /// - /// Will execute after - /// - public int Order => 0; - - /// - public void OnProvidersExecuted(ApplicationModelProviderContext context) - { - } - - /// - public void OnProvidersExecuting(ApplicationModelProviderContext context) - { - foreach (ControllerModel controller in context.Result.Controllers) + foreach (ActionModel action in controller.Actions) { - if (!IsUmbracoApiController(controller)) + foreach (IActionModelConvention convention in _actionModelConventions) { - continue; - } - - foreach (ActionModel action in controller.Actions) - { - foreach (IActionModelConvention convention in _actionModelConventions) - { - convention.Apply(action); - } + convention.Apply(action); } } } - - private static bool IsUmbracoApiController(ICommonModel controller) - => controller.Attributes.OfType().Any(); } + + private static bool IsUmbracoApiController(ICommonModel controller) + => controller.Attributes.OfType().Any(); } diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs index 2eff08d54d..236cb39ce3 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs @@ -4,58 +4,55 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ModelBinders; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +/// +/// Applies the body model binder to any complex parameter and those with a +/// binding source of type +/// +public class UmbracoJsonModelBinderConvention : IActionModelConvention { - /// - /// Applies the body model binder to any complex parameter and those with a - /// binding source of type - /// - public class UmbracoJsonModelBinderConvention : IActionModelConvention + private readonly IModelMetadataProvider _modelMetadataProvider; + + public UmbracoJsonModelBinderConvention() + : this(StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IModelMetadataProvider _modelMetadataProvider; + } - public UmbracoJsonModelBinderConvention() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } + public UmbracoJsonModelBinderConvention(IModelMetadataProvider modelMetadataProvider) => + _modelMetadataProvider = modelMetadataProvider; - public UmbracoJsonModelBinderConvention(IModelMetadataProvider modelMetadataProvider) + /// + public void Apply(ActionModel action) + { + foreach (ParameterModel p in action.Parameters) { - _modelMetadataProvider = modelMetadataProvider; - } - - /// - public void Apply(ActionModel action) - { - foreach (ParameterModel p in action.Parameters) + if (p.BindingInfo == null) { - if (p.BindingInfo == null) + if (IsComplexTypeParameter(p)) { - if (IsComplexTypeParameter(p)) + p.BindingInfo = new BindingInfo { - p.BindingInfo = new BindingInfo - { - BindingSource = BindingSource.Body, - BinderType = typeof(UmbracoJsonModelBinder) - }; - } - - continue; + BindingSource = BindingSource.Body, + BinderType = typeof(UmbracoJsonModelBinder), + }; } - if (p.BindingInfo.BindingSource == BindingSource.Body) - { - p.BindingInfo.BinderType = typeof(UmbracoJsonModelBinder); - } + continue; + } + + if (p.BindingInfo.BindingSource == BindingSource.Body) + { + p.BindingInfo.BinderType = typeof(UmbracoJsonModelBinder); } } + } - private bool IsComplexTypeParameter(ParameterModel parameter) - { - // No need for information from attributes on the parameter. Just use its type. - ModelMetadata metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType); + private bool IsComplexTypeParameter(ParameterModel parameter) + { + // No need for information from attributes on the parameter. Just use its type. + ModelMetadata metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType); - return metadata.IsComplexType; - } + return metadata.IsComplexType; } } diff --git a/src/Umbraco.Web.Common/ApplicationModels/VirtualPageApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/VirtualPageApplicationModelProvider.cs index 7697b9841a..5fb52d8fd0 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/VirtualPageApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/VirtualPageApplicationModelProvider.cs @@ -1,61 +1,58 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +/// +/// Applies the to any action on a controller that is +/// +/// +public class VirtualPageApplicationModelProvider : IApplicationModelProvider { + private readonly List _actionModelConventions = new() { new VirtualPageConvention() }; + + /// /// - /// Applies the to any action on a controller that is + /// Will execute after /// - public class VirtualPageApplicationModelProvider : IApplicationModelProvider + public int Order => 0; + + /// + public void OnProvidersExecuted(ApplicationModelProviderContext context) { - private readonly List _actionModelConventions = new List() + } + + /// + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (ControllerModel controller in context.Result.Controllers) { - new VirtualPageConvention() - }; - - /// - /// - /// Will execute after - /// - public int Order => 0; - - /// - public void OnProvidersExecuted(ApplicationModelProviderContext context) { } - - /// - public void OnProvidersExecuting(ApplicationModelProviderContext context) - { - foreach (ControllerModel controller in context.Result.Controllers) + if (!IsVirtualPageController(controller)) { - if (!IsVirtualPageController(controller)) - { - continue; - } + continue; + } - foreach (ActionModel action in controller.Actions.ToList()) + foreach (ActionModel action in controller.Actions.ToList()) + { + if (action.ActionName == nameof(IVirtualPageController.FindContent) + && action.ActionMethod.ReturnType == typeof(IPublishedContent)) { - if (action.ActionName == nameof(IVirtualPageController.FindContent) - && action.ActionMethod.ReturnType == typeof(IPublishedContent)) + // this is not an action, it's just the implementation of IVirtualPageController + controller.Actions.Remove(action); + } + else + { + foreach (IActionModelConvention convention in _actionModelConventions) { - // this is not an action, it's just the implementation of IVirtualPageController - controller.Actions.Remove(action); - } - else - { - foreach (IActionModelConvention convention in _actionModelConventions) - { - convention.Apply(action); - } + convention.Apply(action); } } } } - - private bool IsVirtualPageController(ControllerModel controller) - => controller.ControllerType.Implements(); } + + private bool IsVirtualPageController(ControllerModel controller) + => controller.ControllerType.Implements(); } diff --git a/src/Umbraco.Web.Common/ApplicationModels/VirtualPageConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/VirtualPageConvention.cs index 66b68c7a85..76e1c8f980 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/VirtualPageConvention.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/VirtualPageConvention.cs @@ -1,14 +1,13 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Umbraco.Cms.Web.Common.Filters; -namespace Umbraco.Cms.Web.Common.ApplicationModels +namespace Umbraco.Cms.Web.Common.ApplicationModels; + +/// +/// Adds the as a convention +/// +public class VirtualPageConvention : IActionModelConvention { - /// - /// Adds the as a convention - /// - public class VirtualPageConvention : IActionModelConvention - { - /// - public void Apply(ActionModel action) => action.Filters.Add(new UmbracoVirtualPageFilterAttribute()); - } + /// + public void Apply(ActionModel action) => action.Filters.Add(new UmbracoVirtualPageFilterAttribute()); } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs index ff431966ce..65c03ffafc 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs @@ -1,54 +1,51 @@ -using System; using System.Collections.Concurrent; -using System.Threading; using Microsoft.Extensions.Hosting; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreApplicationShutdownRegistry : IApplicationShutdownRegistry { - public class AspNetCoreApplicationShutdownRegistry : IApplicationShutdownRegistry + private readonly IHostApplicationLifetime _hostApplicationLifetime; + + private readonly ConcurrentDictionary _registeredObjects = new(); + + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreApplicationShutdownRegistry(IHostApplicationLifetime hostApplicationLifetime) + => _hostApplicationLifetime = hostApplicationLifetime; + + public void RegisterObject(IRegisteredObject registeredObject) { - private readonly IHostApplicationLifetime _hostApplicationLifetime; - private readonly ConcurrentDictionary _registeredObjects = - new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - public AspNetCoreApplicationShutdownRegistry(IHostApplicationLifetime hostApplicationLifetime) - => _hostApplicationLifetime = hostApplicationLifetime; - - public void RegisterObject(IRegisteredObject registeredObject) + var wrapped = new RegisteredObjectWrapper(registeredObject); + if (!_registeredObjects.TryAdd(registeredObject, wrapped)) { - var wrapped = new RegisteredObjectWrapper(registeredObject); - if (!_registeredObjects.TryAdd(registeredObject, wrapped)) - { - throw new InvalidOperationException("Could not register object"); - } - - var cancellationTokenRegistration = _hostApplicationLifetime.ApplicationStopping.Register(() => wrapped.Stop(true)); - wrapped.CancellationTokenRegistration = cancellationTokenRegistration; + throw new InvalidOperationException("Could not register object"); } - public void UnregisterObject(IRegisteredObject registeredObject) + CancellationTokenRegistration cancellationTokenRegistration = + _hostApplicationLifetime.ApplicationStopping.Register(() => wrapped.Stop(true)); + wrapped.CancellationTokenRegistration = cancellationTokenRegistration; + } + + public void UnregisterObject(IRegisteredObject registeredObject) + { + if (_registeredObjects.TryGetValue(registeredObject, out RegisteredObjectWrapper? wrapped)) { - if (_registeredObjects.TryGetValue(registeredObject, out var wrapped)) - { - wrapped.CancellationTokenRegistration.Unregister(); - } - } - - - private class RegisteredObjectWrapper - { - private readonly IRegisteredObject _inner; - - public RegisteredObjectWrapper(IRegisteredObject inner) => _inner = inner; - - public CancellationTokenRegistration CancellationTokenRegistration { get; set; } - - public void Stop(bool immediate) => _inner.Stop(immediate); + wrapped.CancellationTokenRegistration.Unregister(); } } + + private class RegisteredObjectWrapper + { + private readonly IRegisteredObject _inner; + + public RegisteredObjectWrapper(IRegisteredObject inner) => _inner = inner; + + public CancellationTokenRegistration CancellationTokenRegistration { get; set; } + + public void Stop(bool immediate) => _inner.Stop(immediate); + } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs index 7107ab3331..d0f8f7b02b 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs @@ -5,34 +5,39 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Routing; using static Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreBackOfficeInfo : IBackOfficeInfo { - public class AspNetCoreBackOfficeInfo : IBackOfficeInfo + private readonly IOptionsMonitor _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private string? _getAbsoluteUrl; + + public AspNetCoreBackOfficeInfo( + IOptionsMonitor globalSettings, + IHostingEnvironment hostingEnviroment) { - private readonly IOptionsMonitor _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private string? _getAbsoluteUrl; - public AspNetCoreBackOfficeInfo(IOptionsMonitor globalSettings, IHostingEnvironment hostingEnviroment) - { - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnviroment; + _globalSettings = globalSettings; + _hostingEnvironment = hostingEnviroment; + } - } - - public string GetAbsoluteUrl + public string GetAbsoluteUrl + { + get { - get + if (_getAbsoluteUrl is null) { - if (_getAbsoluteUrl is null) + if (_hostingEnvironment.ApplicationMainUrl is null) { - if(_hostingEnvironment.ApplicationMainUrl is null) - { - return ""; - } - _getAbsoluteUrl = WebPath.Combine(_hostingEnvironment.ApplicationMainUrl.ToString(), _globalSettings.CurrentValue.UmbracoPath.TrimStart(CharArrays.TildeForwardSlash)); + return string.Empty; } - return _getAbsoluteUrl; + + _getAbsoluteUrl = WebPath.Combine( + _hostingEnvironment.ApplicationMainUrl.ToString(), + _globalSettings.CurrentValue.UmbracoPath.TrimStart(CharArrays.TildeForwardSlash)); } + + return _getAbsoluteUrl; } } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 478e8aa13d..2f5ff585ed 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -1,49 +1,34 @@ -using System; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreCookieManager : ICookieManager { - public class AspNetCoreCookieManager : ICookieManager + private readonly IHttpContextAccessor _httpContextAccessor; + + public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; + + public void ExpireCookie(string cookieName) { - private readonly IHttpContextAccessor _httpContextAccessor; + HttpContext? httpContext = _httpContextAccessor.HttpContext; - public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) + if (httpContext is null) { - _httpContextAccessor = httpContextAccessor; + return; } - public void ExpireCookie(string cookieName) - { - var httpContext = _httpContextAccessor.HttpContext; - - if (httpContext is null) return; - - var cookieValue = httpContext.Request.Cookies[cookieName]; - - httpContext.Response.Cookies.Append(cookieName, cookieValue ?? string.Empty, new CookieOptions() - { - Expires = DateTime.Now.AddYears(-1) - }); - } - - public string? GetCookieValue(string cookieName) - { - return _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; - } - - public void SetCookieValue(string cookieName, string value) - { - _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, new CookieOptions() - { - - }); - } - - public bool HasCookie(string cookieName) - { - return !(GetCookieValue(cookieName) is null); - } + var cookieValue = httpContext.Request.Cookies[cookieName]; + httpContext.Response.Cookies.Append(cookieName, cookieValue ?? string.Empty, + new CookieOptions { Expires = DateTime.Now.AddYears(-1) }); } + + public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; + + public void SetCookieValue(string cookieName, string value) => + _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, new CookieOptions()); + + public bool HasCookie(string cookieName) => !(GetCookieValue(cookieName) is null); } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 644938082f..57f1e288b7 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Microsoft.AspNetCore.DataProtection.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -12,197 +10,200 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreHostingEnvironment : IHostingEnvironment { - public class AspNetCoreHostingEnvironment : IHostingEnvironment - { - private readonly ConcurrentHashSet _applicationUrls = new ConcurrentHashSet(); - private readonly IOptionsMonitor _hostingSettings; - private readonly IOptionsMonitor _webRoutingSettings; - private readonly IWebHostEnvironment _webHostEnvironment; - private readonly IApplicationDiscriminator? _applicationDiscriminator; + private readonly IApplicationDiscriminator? _applicationDiscriminator; + private readonly ConcurrentHashSet _applicationUrls = new(); + private readonly IOptionsMonitor _hostingSettings; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IOptionsMonitor _webRoutingSettings; - private string? _applicationId; - private string? _localTempPath; + private readonly UrlMode _urlProviderMode; - private UrlMode _urlProviderMode; + private string? _applicationId; + private string? _localTempPath; - [Obsolete("Please use an alternative constructor.")] - public AspNetCoreHostingEnvironment( - IServiceProvider serviceProvider, - IOptionsMonitor hostingSettings, - IOptionsMonitor webRoutingSettings, - IWebHostEnvironment webHostEnvironment) + [Obsolete("Please use an alternative constructor.")] + public AspNetCoreHostingEnvironment( + IServiceProvider serviceProvider, + IOptionsMonitor hostingSettings, + IOptionsMonitor webRoutingSettings, + IWebHostEnvironment webHostEnvironment) : this(hostingSettings, webRoutingSettings, webHostEnvironment, serviceProvider.GetService()!) + { + } + + public AspNetCoreHostingEnvironment( + IOptionsMonitor hostingSettings, + IOptionsMonitor webRoutingSettings, + IWebHostEnvironment webHostEnvironment, + IApplicationDiscriminator applicationDiscriminator) + : this(hostingSettings, webRoutingSettings, webHostEnvironment) => + _applicationDiscriminator = applicationDiscriminator; + + public AspNetCoreHostingEnvironment( + IOptionsMonitor hostingSettings, + IOptionsMonitor webRoutingSettings, + IWebHostEnvironment webHostEnvironment) + { + _hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings)); + _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); + _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); + _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; + + SetSiteName(hostingSettings.CurrentValue.SiteName); + + // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack + // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. + // See summery of OptionsMonitorAdapter for more information. + if (hostingSettings is OptionsMonitor) { + hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); } - public AspNetCoreHostingEnvironment( - IOptionsMonitor hostingSettings, - IOptionsMonitor webRoutingSettings, - IWebHostEnvironment webHostEnvironment, - IApplicationDiscriminator applicationDiscriminator) - : this(hostingSettings, webRoutingSettings, webHostEnvironment) => - _applicationDiscriminator = applicationDiscriminator; + ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; - public AspNetCoreHostingEnvironment( - IOptionsMonitor hostingSettings, - IOptionsMonitor webRoutingSettings, - IWebHostEnvironment webHostEnvironment) + if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) { - _hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings)); - _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); - _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; - - SetSiteName(hostingSettings.CurrentValue.SiteName); - - // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack - // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. - // See summery of OptionsMonitorAdapter for more information. - if (hostingSettings is OptionsMonitor) - { - hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); - } - - ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; - - if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) - { - ApplicationMainUrl = new Uri(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl); - } + ApplicationMainUrl = new Uri(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl); } + } - /// - public bool IsHosted { get; } = true; + // Scheduled for removal in v12 + [Obsolete("This will never have a value")] + public Version? IISVersion { get; } - /// - public Uri ApplicationMainUrl { get; private set; } = null!; + /// + public bool IsHosted { get; } = true; - /// - public string SiteName { get; private set; } = null!; + /// + public Uri ApplicationMainUrl { get; private set; } = null!; - /// - public string ApplicationId + /// + public string SiteName { get; private set; } = null!; + + /// + public string ApplicationId + { + get { - get + if (_applicationId != null) { - if (_applicationId != null) - { - return _applicationId; - } - - _applicationId = _applicationDiscriminator?.GetApplicationId() ?? _webHostEnvironment.GetTemporaryApplicationId(); - return _applicationId; } + + _applicationId = _applicationDiscriminator?.GetApplicationId() ?? + _webHostEnvironment.GetTemporaryApplicationId(); + + return _applicationId; } - - /// - public string ApplicationPhysicalPath { get; } - - // TODO how to find this, This is a server thing, not application thing. - public string ApplicationVirtualPath => _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; - - /// - public bool IsDebugMode => _hostingSettings.CurrentValue.Debug; - - public Version? IISVersion { get; } - - public string LocalTempPath - { - get - { - if (_localTempPath != null) - { - return _localTempPath; - } - - switch (_hostingSettings.CurrentValue.LocalTempStorageLocation) - { - case LocalTempStorage.EnvironmentTemp: - - // environment temp is unique, we need a folder per site - - // use a hash - // combine site name and application id - // site name is a Guid on Cloud - // application id is eg /LM/W3SVC/123456/ROOT - // the combination is unique on one server - // and, if a site moves from worker A to B and then back to A... - // hopefully it gets a new Guid or new application id? - string hashString = SiteName + "::" + ApplicationId; - string hash = hashString.GenerateHash(); - string siteTemp = Path.Combine(Path.GetTempPath(), "UmbracoData", hash); - - return _localTempPath = siteTemp; - - default: - - return _localTempPath = MapPathContentRoot(Cms.Core.Constants.SystemDirectories.TempData); - } - } - } - - /// - public string MapPathWebRoot(string path) => _webHostEnvironment.MapPathWebRoot(path); - - /// - public string MapPathContentRoot(string path) => _webHostEnvironment.MapPathContentRoot(path); - - /// - public string ToAbsolute(string virtualPath) - { - if (!virtualPath.StartsWith("~/") && !virtualPath.StartsWith("/") && _urlProviderMode != UrlMode.Absolute) - { - throw new InvalidOperationException($"The value {virtualPath} for parameter {nameof(virtualPath)} must start with ~/ or /"); - } - - // will occur if it starts with "/" - if (Uri.IsWellFormedUriString(virtualPath, UriKind.Absolute)) - { - return virtualPath; - } - - string fullPath = ApplicationVirtualPath.EnsureEndsWith('/') + virtualPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); - - return fullPath; - } - - public void EnsureApplicationMainUrl(Uri? currentApplicationUrl) - { - // Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that - // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part - // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 - - // see U4-10626 - in some cases we want to reset the application url - // (this is a simplified version of what was in 7.x) - // note: should this be optional? is it expensive? - - - if (currentApplicationUrl is null) - { - return; - } - - if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) - { - return; - } - - var change = !_applicationUrls.Contains(currentApplicationUrl); - if (change) - { - if (_applicationUrls.TryAdd(currentApplicationUrl)) - { - ApplicationMainUrl = currentApplicationUrl; - } - } - } - - private void SetSiteName(string? siteName) => - SiteName = string.IsNullOrWhiteSpace(siteName) - ? _webHostEnvironment.ApplicationName - : siteName; } + + /// + public string ApplicationPhysicalPath { get; } + + // TODO how to find this, This is a server thing, not application thing. + public string ApplicationVirtualPath => + _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; + + /// + public bool IsDebugMode => _hostingSettings.CurrentValue.Debug; + + public string LocalTempPath + { + get + { + if (_localTempPath != null) + { + return _localTempPath; + } + + switch (_hostingSettings.CurrentValue.LocalTempStorageLocation) + { + case LocalTempStorage.EnvironmentTemp: + + // environment temp is unique, we need a folder per site + + // use a hash + // combine site name and application id + // site name is a Guid on Cloud + // application id is eg /LM/W3SVC/123456/ROOT + // the combination is unique on one server + // and, if a site moves from worker A to B and then back to A... + // hopefully it gets a new Guid or new application id? + var hashString = SiteName + "::" + ApplicationId; + var hash = hashString.GenerateHash(); + var siteTemp = Path.Combine(Path.GetTempPath(), "UmbracoData", hash); + + return _localTempPath = siteTemp; + + default: + + return _localTempPath = MapPathContentRoot(Core.Constants.SystemDirectories.TempData); + } + } + } + + /// + public string MapPathWebRoot(string path) => _webHostEnvironment.MapPathWebRoot(path); + + /// + public string MapPathContentRoot(string path) => _webHostEnvironment.MapPathContentRoot(path); + + /// + public string ToAbsolute(string virtualPath) + { + if (!virtualPath.StartsWith("~/") && !virtualPath.StartsWith("/") && _urlProviderMode != UrlMode.Absolute) + { + throw new InvalidOperationException( + $"The value {virtualPath} for parameter {nameof(virtualPath)} must start with ~/ or /"); + } + + // will occur if it starts with "/" + if (Uri.IsWellFormedUriString(virtualPath, UriKind.Absolute)) + { + return virtualPath; + } + + var fullPath = ApplicationVirtualPath.EnsureEndsWith('/') + + virtualPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); + + return fullPath; + } + + public void EnsureApplicationMainUrl(Uri? currentApplicationUrl) + { + // Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url + // (this is a simplified version of what was in 7.x) + // note: should this be optional? is it expensive? + if (currentApplicationUrl is null) + { + return; + } + + if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) + { + return; + } + + var change = !_applicationUrls.Contains(currentApplicationUrl); + if (change) + { + if (_applicationUrls.TryAdd(currentApplicationUrl)) + { + ApplicationMainUrl = currentApplicationUrl; + } + } + } + + private void SetSiteName(string? siteName) => + SiteName = string.IsNullOrWhiteSpace(siteName) + ? _webHostEnvironment.ApplicationName + : siteName; } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs index d7683e1ffe..d9bc76f8fa 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs @@ -1,17 +1,14 @@ using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Net; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreIpResolver : IIpResolver { - public class AspNetCoreIpResolver : IIpResolver - { - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; - public AspNetCoreIpResolver(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } + public AspNetCoreIpResolver(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - public string GetCurrentRequestIpAddress() => _httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? string.Empty; - } + public string GetCurrentRequestIpAddress() => + _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs index bd9d4439e2..dbf9a2d4d3 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs @@ -1,13 +1,10 @@ -using System; using System.Runtime.InteropServices; using Umbraco.Cms.Core.Diagnostics; -namespace Umbraco.Cms.Web.Common.AspNetCore -{ +namespace Umbraco.Cms.Web.Common.AspNetCore; - public class AspNetCoreMarchal : IMarchal - { - // This thing is not available in net standard, but exists in both .Net 4 and .Net Core 3 - public IntPtr GetExceptionPointers() => Marshal.GetExceptionPointers(); - } +public class AspNetCoreMarchal : IMarchal +{ + // This thing is not available in net standard, but exists in both .Net 4 and .Net Core 3 + public IntPtr GetExceptionPointers() => Marshal.GetExceptionPointers(); } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCorePasswordHasher.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCorePasswordHasher.cs index 17c1306789..d9aed0be81 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCorePasswordHasher.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCorePasswordHasher.cs @@ -1,19 +1,13 @@ using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCorePasswordHasher : IPasswordHasher { - public class AspNetCorePasswordHasher : Cms.Core.Security.IPasswordHasher - { - private PasswordHasher _underlyingHasher; + private readonly PasswordHasher _underlyingHasher; - public AspNetCorePasswordHasher() - { - _underlyingHasher = new PasswordHasher(); - } + public AspNetCorePasswordHasher() => _underlyingHasher = new PasswordHasher(); - public string HashPassword(string password) - { - return _underlyingHasher.HashPassword(null!, password); - } - } + public string HashPassword(string password) => _underlyingHasher.HashPassword(null!, password); } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs index 9924ae9109..38d67ff2f0 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Options; @@ -10,92 +7,93 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler { - public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler + private readonly ISet _applicationUrls = new HashSet(); + private readonly IHttpContextAccessor _httpContextAccessor; + private Uri? _currentApplicationUrl; + private bool _hasAppUrl; + private object _initLocker = new(); + private bool _isInit; + private WebRoutingSettings _webRoutingSettings; + + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreRequestAccessor( + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor webRoutingSettings) { - private readonly IHttpContextAccessor _httpContextAccessor; - private WebRoutingSettings _webRoutingSettings; - private readonly ISet _applicationUrls = new HashSet(); - private Uri? _currentApplicationUrl; - private object _initLocker = new object(); - private bool _hasAppUrl = false; - private bool _isInit = false; + _httpContextAccessor = httpContextAccessor; + _webRoutingSettings = webRoutingSettings.CurrentValue; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - /// - /// Initializes a new instance of the class. - /// - public AspNetCoreRequestAccessor( - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor webRoutingSettings) + /// + /// This just initializes the application URL on first request attempt + /// TODO: This doesn't belong here, the GetApplicationUrl doesn't belong to IRequestAccessor + /// this should be part of middleware not a lazy init based on an INotification + /// + public void Handle(UmbracoRequestBeginNotification notification) + => LazyInitializer.EnsureInitialized(ref _hasAppUrl, ref _isInit, ref _initLocker, () => { - _httpContextAccessor = httpContextAccessor; - _webRoutingSettings = webRoutingSettings.CurrentValue; - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + GetApplicationUrl(); + return true; + }); + /// + public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); + + /// + public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name]; + + /// + public Uri? GetRequestUrl() => _httpContextAccessor.HttpContext != null + ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) + : null; + + public Uri? GetApplicationUrl() + { + // Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url + // (this is a simplified version of what was in 7.x) + // note: should this be optional? is it expensive? + if (!(_webRoutingSettings.UmbracoApplicationUrl is null)) + { + return new Uri(_webRoutingSettings.UmbracoApplicationUrl); } - /// - public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); + HttpRequest? request = _httpContextAccessor.HttpContext?.Request; - private string? GetFormValue(string name) + if (request is null) { - var request = _httpContextAccessor.GetRequiredHttpContext().Request; - if (!request.HasFormContentType) - return null; - return request.Form[name]; - } - - /// - public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name]; - - /// - public Uri? GetRequestUrl() => _httpContextAccessor.HttpContext != null ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) : null; - - /// - public Uri? GetApplicationUrl() - { - // Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that - // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part - // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 - - // see U4-10626 - in some cases we want to reset the application url - // (this is a simplified version of what was in 7.x) - // note: should this be optional? is it expensive? - - if (!(_webRoutingSettings.UmbracoApplicationUrl is null)) - { - return new Uri(_webRoutingSettings.UmbracoApplicationUrl); - } - - var request = _httpContextAccessor.HttpContext?.Request; - - if (request is null) - { - return _currentApplicationUrl; - } - - var url = UriHelper.BuildAbsolute(request.Scheme, request.Host); - if (url != null && !_applicationUrls.Contains(url)) - { - _applicationUrls.Add(url); - - _currentApplicationUrl ??= new Uri(url); - } - return _currentApplicationUrl; } - /// - /// This just initializes the application URL on first request attempt - /// TODO: This doesn't belong here, the GetApplicationUrl doesn't belong to IRequestAccessor - /// this should be part of middleware not a lazy init based on an INotification - /// - public void Handle(UmbracoRequestBeginNotification notification) - => LazyInitializer.EnsureInitialized(ref _hasAppUrl, ref _isInit, ref _initLocker, () => - { - GetApplicationUrl(); - return true; - }); + var url = UriHelper.BuildAbsolute(request.Scheme, request.Host); + if (!_applicationUrls.Contains(url)) + { + _applicationUrls.Add(url); + + _currentApplicationUrl ??= new Uri(url); + } + + return _currentApplicationUrl; + } + + private string? GetFormValue(string name) + { + HttpRequest request = _httpContextAccessor.GetRequiredHttpContext().Request; + if (!request.HasFormContentType) + { + return null; + } + + return request.Form[name]; } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs index 5f71e0c02a..1813c75932 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs @@ -3,63 +3,59 @@ using Microsoft.AspNetCore.Http.Features; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +internal class AspNetCoreSessionManager : ISessionIdResolver, ISessionManager { - internal class AspNetCoreSessionManager : ISessionIdResolver, ISessionManager + private readonly IHttpContextAccessor _httpContextAccessor; + + public AspNetCoreSessionManager(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; + + public string? SessionId { - private readonly IHttpContextAccessor _httpContextAccessor; - - public AspNetCoreSessionManager(IHttpContextAccessor httpContextAccessor) + get { - _httpContextAccessor = httpContextAccessor; - } + HttpContext? httpContext = _httpContextAccessor.HttpContext; - /// - /// If session isn't enabled this will throw an exception so we check - /// - private bool IsSessionsAvailable => !(_httpContextAccessor.HttpContext?.Features.Get() is null); - - public string? SessionId - { - get - { - HttpContext? httpContext = _httpContextAccessor?.HttpContext; - - return IsSessionsAvailable - ? httpContext?.Session?.Id - : "0"; - } - } - - public string? GetSessionValue(string key) - { - if (!IsSessionsAvailable) - { - return null; - } - - return _httpContextAccessor.HttpContext?.Session.GetString(key); - } - - - public void SetSessionValue(string key, string value) - { - if (!IsSessionsAvailable) - { - return; - } - - _httpContextAccessor.HttpContext?.Session.SetString(key, value); - } - - public void ClearSessionValue(string key) - { - if (!IsSessionsAvailable) - { - return; - } - - _httpContextAccessor.HttpContext?.Session.Remove(key); + return IsSessionsAvailable + ? httpContext?.Session.Id + : "0"; } } + + /// + /// If session isn't enabled this will throw an exception so we check + /// + private bool IsSessionsAvailable => !(_httpContextAccessor.HttpContext?.Features.Get() is null); + + public string? GetSessionValue(string key) + { + if (!IsSessionsAvailable) + { + return null; + } + + return _httpContextAccessor.HttpContext?.Session.GetString(key); + } + + public void SetSessionValue(string key, string value) + { + if (!IsSessionsAvailable) + { + return; + } + + _httpContextAccessor.HttpContext?.Session.SetString(key, value); + } + + public void ClearSessionValue(string key) + { + if (!IsSessionsAvailable) + { + return; + } + + _httpContextAccessor.HttpContext?.Session.Remove(key); + } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 2bda7a28a7..0fbb4d0a48 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -1,23 +1,20 @@ using Microsoft.Extensions.Hosting; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime { - public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime + private readonly IHostApplicationLifetime _hostApplicationLifetime; + + public AspNetCoreUmbracoApplicationLifetime(IHostApplicationLifetime hostApplicationLifetime) => + _hostApplicationLifetime = hostApplicationLifetime; + + public bool IsRestarting { get; set; } + + public void Restart() { - private readonly IHostApplicationLifetime _hostApplicationLifetime; - - public AspNetCoreUmbracoApplicationLifetime(IHostApplicationLifetime hostApplicationLifetime) - { - _hostApplicationLifetime = hostApplicationLifetime; - } - - public bool IsRestarting { get; set; } - - public void Restart() - { - IsRestarting = true; - _hostApplicationLifetime.StopApplication(); - } + IsRestarting = true; + _hostApplicationLifetime.StopApplication(); } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs index d8d23e32f6..1b548de227 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs @@ -1,20 +1,14 @@ using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Net; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +public class AspNetCoreUserAgentProvider : IUserAgentProvider { - public class AspNetCoreUserAgentProvider : IUserAgentProvider - { - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; - public AspNetCoreUserAgentProvider(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } + public AspNetCoreUserAgentProvider(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; - public string? GetUserAgent() - { - return _httpContextAccessor.HttpContext?.Request.Headers["User-Agent"].ToString(); - } - } + public string? GetUserAgent() => _httpContextAccessor.HttpContext?.Request.Headers["User-Agent"].ToString(); } diff --git a/src/Umbraco.Web.Common/AspNetCore/OptionsMonitorAdapter.cs b/src/Umbraco.Web.Common/AspNetCore/OptionsMonitorAdapter.cs index 5811bf45ec..112841a722 100644 --- a/src/Umbraco.Web.Common/AspNetCore/OptionsMonitorAdapter.cs +++ b/src/Umbraco.Web.Common/AspNetCore/OptionsMonitorAdapter.cs @@ -1,33 +1,21 @@ -using System; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Web.Common.AspNetCore +namespace Umbraco.Cms.Web.Common.AspNetCore; + +/// +/// HACK: OptionsMonitor but without the monitoring, hopefully temporary. +/// This is just used so we can get an AspNetCoreHostingEnvironment to +/// build a TypeLoader long before ServiceProvider is built. +/// +[Obsolete("Please let the container wire up a real OptionsMonitor for you")] +internal class OptionsMonitorAdapter : IOptionsMonitor + where T : class, new() { - /// - /// HACK: OptionsMonitor but without the monitoring, hopefully temporary. - /// This is just used so we can get an AspNetCoreHostingEnvironment to - /// build a TypeLoader long before ServiceProvider is built. - /// - [Obsolete("Please let the container wire up a real OptionsMonitor for you")] - internal class OptionsMonitorAdapter : IOptionsMonitor where T : class, new() - { - private readonly T _inner; + public OptionsMonitorAdapter(T inner) => CurrentValue = inner ?? throw new ArgumentNullException(nameof(inner)); - public OptionsMonitorAdapter(T inner) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } + public T CurrentValue { get; } - public T Get(string name) - { - return _inner; - } + public T Get(string name) => CurrentValue; - public IDisposable OnChange(Action listener) - { - throw new NotImplementedException(); - } - - public T CurrentValue => _inner; - } + public IDisposable OnChange(Action listener) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs b/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs index 8fad2b6c6b..be616db7d0 100644 --- a/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs +++ b/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs @@ -1,13 +1,11 @@ -using System; +namespace Umbraco.Cms.Web.Common.Attributes; -namespace Umbraco.Cms.Web.Common.Attributes +/// +/// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if +/// it +/// is a back office route or not. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class IgnoreFromNotFoundSelectorPolicyAttribute : Attribute { - /// - /// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if it - /// is a back office route or not. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class IgnoreFromNotFoundSelectorPolicyAttribute : Attribute - { - } } diff --git a/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs b/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs index 15c2d45267..350d32fb71 100644 --- a/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs +++ b/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs @@ -1,13 +1,11 @@ -using System; +namespace Umbraco.Cms.Web.Common.Attributes; -namespace Umbraco.Cms.Web.Common.Attributes +/// +/// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if +/// it +/// is a back office route or not. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class IsBackOfficeAttribute : Attribute { - /// - /// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if it - /// is a back office route or not. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class IsBackOfficeAttribute : Attribute - { - } } diff --git a/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs index 885558eb65..5d06541f65 100644 --- a/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs +++ b/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs @@ -1,31 +1,32 @@ -using System; -using System.Linq; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Cms.Web.Common.Attributes +namespace Umbraco.Cms.Web.Common.Attributes; + +/// +/// Indicates that a controller is a plugin controller and will be routed to its own area. +/// +[AttributeUsage(AttributeTargets.Class)] +public class PluginControllerAttribute : AreaAttribute { /// - /// Indicates that a controller is a plugin controller and will be routed to its own area. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class PluginControllerAttribute : AreaAttribute + /// + public PluginControllerAttribute(string areaName) + : base(areaName) { - /// - /// Initializes a new instance of the class. - /// - /// - public PluginControllerAttribute(string areaName) : base(areaName) + // validate this, only letters and digits allowed. + if (areaName.Any(c => !char.IsLetterOrDigit(c))) { - // validate this, only letters and digits allowed. - if (areaName.Any(c => !char.IsLetterOrDigit(c))) - throw new FormatException($"Invalid area name \"{areaName}\": the area name can only contains letters and digits."); - - AreaName = areaName; + throw new FormatException( + $"Invalid area name \"{areaName}\": the area name can only contains letters and digits."); } - /// - /// Gets the name of the area. - /// - public string AreaName { get; } + AreaName = areaName; } + + /// + /// Gets the name of the area. + /// + public string AreaName { get; } } diff --git a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs index abb2e4ff06..48d3f3404e 100644 --- a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs +++ b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs @@ -1,13 +1,11 @@ -using System; using Umbraco.Cms.Web.Common.ApplicationModels; -namespace Umbraco.Cms.Web.Common.Attributes +namespace Umbraco.Cms.Web.Common.Attributes; + +/// +/// When present on a controller then conventions will apply +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class UmbracoApiControllerAttribute : Attribute { - /// - /// When present on a controller then conventions will apply - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class UmbracoApiControllerAttribute : Attribute - { - } } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 0ef34a9ced..50e399d4f0 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -1,82 +1,76 @@ -namespace Umbraco.Cms.Web.Common.Authorization +namespace Umbraco.Cms.Web.Common.Authorization; + +/// +/// A list of authorization policy names for use in the back office +/// +public static class AuthorizationPolicies { + public const string UmbracoFeatureEnabled = nameof(UmbracoFeatureEnabled); + + public const string BackOfficeAccess = nameof(BackOfficeAccess); + public const string BackOfficeAccessWithoutApproval = nameof(BackOfficeAccessWithoutApproval); + public const string UserBelongsToUserGroupInRequest = nameof(UserBelongsToUserGroupInRequest); + public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); + public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); + + // Content permission access + public const string ContentPermissionByResource = nameof(ContentPermissionByResource); + public const string ContentPermissionEmptyRecycleBin = nameof(ContentPermissionEmptyRecycleBin); + public const string ContentPermissionAdministrationById = nameof(ContentPermissionAdministrationById); + public const string ContentPermissionPublishById = nameof(ContentPermissionPublishById); + public const string ContentPermissionRollbackById = nameof(ContentPermissionRollbackById); + public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); + public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); + public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + + public const string MediaPermissionByResource = nameof(MediaPermissionByResource); + public const string MediaPermissionPathById = nameof(MediaPermissionPathById); + + // Single section access + public const string SectionAccessContent = nameof(SectionAccessContent); + public const string SectionAccessPackages = nameof(SectionAccessPackages); + public const string SectionAccessUsers = nameof(SectionAccessUsers); + public const string SectionAccessMedia = nameof(SectionAccessMedia); + public const string SectionAccessSettings = nameof(SectionAccessSettings); + public const string SectionAccessMembers = nameof(SectionAccessMembers); + + // Custom access based on multiple sections + public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); + public const string SectionAccessForTinyMce = nameof(SectionAccessForTinyMce); + public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); + public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); + public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); + public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); + + // Single tree access + public const string TreeAccessDocuments = nameof(TreeAccessDocuments); + public const string TreeAccessUsers = nameof(TreeAccessUsers); + public const string TreeAccessPartialViews = nameof(TreeAccessPartialViews); + public const string TreeAccessPartialViewMacros = nameof(TreeAccessPartialViewMacros); + public const string TreeAccessDataTypes = nameof(TreeAccessDataTypes); + public const string TreeAccessPackages = nameof(TreeAccessPackages); + public const string TreeAccessLogs = nameof(TreeAccessLogs); + public const string TreeAccessTemplates = nameof(TreeAccessTemplates); + public const string TreeAccessDictionary = nameof(TreeAccessDictionary); + public const string TreeAccessRelationTypes = nameof(TreeAccessRelationTypes); + public const string TreeAccessMediaTypes = nameof(TreeAccessMediaTypes); + public const string TreeAccessMacros = nameof(TreeAccessMacros); + public const string TreeAccessLanguages = nameof(TreeAccessLanguages); + public const string TreeAccessMemberGroups = nameof(TreeAccessMemberGroups); + public const string TreeAccessDocumentTypes = nameof(TreeAccessDocumentTypes); + public const string TreeAccessMemberTypes = nameof(TreeAccessMemberTypes); + + // Custom access based on multiple trees + public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes); + public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes); + public const string TreeAccessMembersOrMemberTypes = nameof(TreeAccessMembersOrMemberTypes); + public const string TreeAccessAnySchemaTypes = nameof(TreeAccessAnySchemaTypes); + public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates); + /// - /// A list of authorization policy names for use in the back office + /// Defines access based on if the user has access to any tree's exposing any types of content (documents, media, + /// members) + /// or any content types (document types, media types, member types) /// - public static class AuthorizationPolicies - { - public const string UmbracoFeatureEnabled = nameof(UmbracoFeatureEnabled); - - public const string BackOfficeAccess = nameof(BackOfficeAccess); - public const string BackOfficeAccessWithoutApproval = nameof(BackOfficeAccessWithoutApproval); - public const string UserBelongsToUserGroupInRequest = nameof(UserBelongsToUserGroupInRequest); - public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); - public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); - - // Content permission access - - public const string ContentPermissionByResource = nameof(ContentPermissionByResource); - public const string ContentPermissionEmptyRecycleBin = nameof(ContentPermissionEmptyRecycleBin); - public const string ContentPermissionAdministrationById = nameof(ContentPermissionAdministrationById); - public const string ContentPermissionPublishById = nameof(ContentPermissionPublishById); - public const string ContentPermissionRollbackById = nameof(ContentPermissionRollbackById); - public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); - public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); - public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); - - public const string MediaPermissionByResource = nameof(MediaPermissionByResource); - public const string MediaPermissionPathById = nameof(MediaPermissionPathById); - - - // Single section access - - public const string SectionAccessContent = nameof(SectionAccessContent); - public const string SectionAccessPackages = nameof(SectionAccessPackages); - public const string SectionAccessUsers = nameof(SectionAccessUsers); - public const string SectionAccessMedia = nameof(SectionAccessMedia); - public const string SectionAccessSettings = nameof(SectionAccessSettings); - public const string SectionAccessMembers = nameof(SectionAccessMembers); - - // Custom access based on multiple sections - - public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); - public const string SectionAccessForTinyMce = nameof(SectionAccessForTinyMce); - public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); - public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); - public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); - public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); - - // Single tree access - - public const string TreeAccessDocuments = nameof(TreeAccessDocuments); - public const string TreeAccessUsers = nameof(TreeAccessUsers); - public const string TreeAccessPartialViews = nameof(TreeAccessPartialViews); - public const string TreeAccessPartialViewMacros = nameof(TreeAccessPartialViewMacros); - public const string TreeAccessDataTypes = nameof(TreeAccessDataTypes); - public const string TreeAccessPackages = nameof(TreeAccessPackages); - public const string TreeAccessLogs = nameof(TreeAccessLogs); - public const string TreeAccessTemplates = nameof(TreeAccessTemplates); - public const string TreeAccessDictionary = nameof(TreeAccessDictionary); - public const string TreeAccessRelationTypes = nameof(TreeAccessRelationTypes); - public const string TreeAccessMediaTypes = nameof(TreeAccessMediaTypes); - public const string TreeAccessMacros = nameof(TreeAccessMacros); - public const string TreeAccessLanguages = nameof(TreeAccessLanguages); - public const string TreeAccessMemberGroups = nameof(TreeAccessMemberGroups); - public const string TreeAccessDocumentTypes = nameof(TreeAccessDocumentTypes); - public const string TreeAccessMemberTypes = nameof(TreeAccessMemberTypes); - - // Custom access based on multiple trees - - public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes); - public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes); - public const string TreeAccessMembersOrMemberTypes = nameof(TreeAccessMembersOrMemberTypes); - public const string TreeAccessAnySchemaTypes = nameof(TreeAccessAnySchemaTypes); - public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates); - - /// - /// Defines access based on if the user has access to any tree's exposing any types of content (documents, media, members) - /// or any content types (document types, media types, member types) - /// - public const string TreeAccessAnyContentOrTypes = nameof(TreeAccessAnyContentOrTypes); - } + public const string TreeAccessAnyContentOrTypes = nameof(TreeAccessAnyContentOrTypes); } diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs index bc025d4f06..485208064d 100644 --- a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs @@ -1,74 +1,74 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Cms.Core.Features; -namespace Umbraco.Cms.Web.Common.Authorization +namespace Umbraco.Cms.Web.Common.Authorization; + +/// +/// Ensures that the controller is an authorized feature. +/// +public class FeatureAuthorizeHandler : AuthorizationHandler { - /// - /// Ensures that the controller is an authorized feature. - /// - public class FeatureAuthorizeHandler : AuthorizationHandler + private readonly UmbracoFeatures _umbracoFeatures; + + public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures) => _umbracoFeatures = umbracoFeatures; + + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + FeatureAuthorizeRequirement requirement) { - private readonly UmbracoFeatures _umbracoFeatures; - - public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures) + var allowed = IsAllowed(context); + if (!allowed.HasValue || allowed.Value) { - _umbracoFeatures = umbracoFeatures; + context.Succeed(requirement); + } + else + { + context.Fail(); } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FeatureAuthorizeRequirement requirement) + return Task.CompletedTask; + } + + private bool? IsAllowed(AuthorizationHandlerContext context) + { + Endpoint? endpoint = null; + + switch (context.Resource) { - var allowed = IsAllowed(context); - if (!allowed.HasValue || allowed.Value) + case DefaultHttpContext defaultHttpContext: { - context.Succeed(requirement); + IEndpointFeature? endpointFeature = defaultHttpContext.Features.Get(); + endpoint = endpointFeature?.Endpoint; + break; } - else + + case AuthorizationFilterContext authorizationFilterContext: { - context.Fail(); + IEndpointFeature? endpointFeature = + authorizationFilterContext.HttpContext.Features.Get(); + endpoint = endpointFeature?.Endpoint; + break; + } + + case Endpoint resourceEndpoint: + { + endpoint = resourceEndpoint; + break; } - return Task.CompletedTask; } - private bool? IsAllowed(AuthorizationHandlerContext context) + if (endpoint is null) { - Endpoint? endpoint = null; - - switch (context.Resource) - { - case DefaultHttpContext defaultHttpContext: - { - IEndpointFeature? endpointFeature = defaultHttpContext.Features.Get(); - endpoint = endpointFeature?.Endpoint; - break; - } - - case Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext authorizationFilterContext: - { - IEndpointFeature? endpointFeature = authorizationFilterContext.HttpContext.Features.Get(); - endpoint = endpointFeature?.Endpoint; - break; - } - - case Endpoint resourceEndpoint: - { - endpoint = resourceEndpoint; - break; - } - } - - if (endpoint is null) - { - throw new InvalidOperationException("This authorization handler can only be applied to controllers routed with endpoint routing"); - } - - var actionDescriptor = endpoint.Metadata.GetMetadata(); - var controllerType = actionDescriptor?.ControllerTypeInfo.AsType(); - return _umbracoFeatures.IsControllerEnabled(controllerType); + throw new InvalidOperationException( + "This authorization handler can only be applied to controllers routed with endpoint routing"); } + + ControllerActionDescriptor? actionDescriptor = endpoint.Metadata.GetMetadata(); + Type? controllerType = actionDescriptor?.ControllerTypeInfo.AsType(); + return _umbracoFeatures.IsControllerEnabled(controllerType); } } diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs index 93605192aa..828a7d2b95 100644 --- a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs @@ -1,16 +1,10 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; -namespace Umbraco.Cms.Web.Common.Authorization +namespace Umbraco.Cms.Web.Common.Authorization; + +/// +/// Authorization requirement for the +/// +public class FeatureAuthorizeRequirement : IAuthorizationRequirement { - - /// - /// Authorization requirement for the - /// - public class FeatureAuthorizeRequirement : IAuthorizationRequirement - { - public FeatureAuthorizeRequirement() - { - } - - } } diff --git a/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs b/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs index 6f4455dc74..53c777c63e 100644 --- a/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs +++ b/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs @@ -1,263 +1,266 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements a on top of +/// +/// +/// +/// The HttpContext is not thread safe and no part of it is which means we need to include our own thread +/// safety mechanisms. This relies on notifications: and +/// +/// in order to facilitate the correct locking and releasing allocations. +/// +/// +public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache { + private readonly IHttpContextAccessor _httpContextAccessor; + /// - /// Implements a on top of + /// Initializes a new instance of the class with a context, for unit tests! /// - /// - /// The HttpContext is not thread safe and no part of it is which means we need to include our own thread - /// safety mechanisms. This relies on notifications: and - /// in order to facilitate the correct locking and releasing allocations. - /// - /// - public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache + public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; + + public bool IsAvailable => TryGetContextItems(out _); + + /// + public override object? Get(string key, Func factory) { - private readonly IHttpContextAccessor _httpContextAccessor; - - /// - /// Initializes a new instance of the class with a context, for unit tests! - /// - public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - - public bool IsAvailable => TryGetContextItems(out _); - - private bool TryGetContextItems([MaybeNullWhen(false)] out IDictionary items) + // no place to cache so just return the callback result + if (!TryGetContextItems(out IDictionary? items)) { - items = _httpContextAccessor.HttpContext?.Items; - return items != null; + return factory(); } - /// - public override object? Get(string key, Func factory) + key = GetCacheKey(key); + + Lazy? result; + + try { - //no place to cache so just return the callback result - if (!TryGetContextItems(out var items)) + EnterWriteLock(); + result = items[key] as Lazy; // null if key not found + + // cannot create value within the lock, so if result.IsValueCreated is false, just + // do nothing here - means that if creation throws, a race condition could cause + // more than one thread to reach the return statement below and throw - accepted. + // get non-created as NonCreatedValue & exceptions as null + if (result == null || + SafeLazy.GetSafeLazyValue(result, true) == + null) { - return factory(); + result = SafeLazy.GetSafeLazy(factory); + items[key] = result; } - - key = GetCacheKey(key); - - Lazy? result; - - try - { - EnterWriteLock(); - result = items[key] as Lazy; // null if key not found - - // cannot create value within the lock, so if result.IsValueCreated is false, just - // do nothing here - means that if creation throws, a race condition could cause - // more than one thread to reach the return statement below and throw - accepted. - - if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null - { - result = SafeLazy.GetSafeLazy(factory); - items[key] = result; - } - } - finally - { - ExitWriteLock(); - } - - // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache - // exceptions (but try again and again) and silently eat them - however at - // some point we have to report them - so need to re-throw here - - // this does not throw anymore - //return result.Value; - - var value = result.Value; // will not throw (safe lazy) - if (value is SafeLazy.ExceptionHolder eh) - { - eh.Exception.Throw(); // throw once! - } - - return value; + } + finally + { + ExitWriteLock(); } - public bool Set(string key, object? value) + // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache + // exceptions (but try again and again) and silently eat them - however at + // some point we have to report them - so need to re-throw here + + // this does not throw anymore + // return result.Value; + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) { - //no place to cache so just return the callback result - if (!TryGetContextItems(out var items)) - { - return false; - } - - key = GetCacheKey(key); - try - { - - EnterWriteLock(); - items[key] = SafeLazy.GetSafeLazy(() => value); - } - finally - { - ExitWriteLock(); - } - return true; + eh.Exception.Throw(); // throw once! } - public bool Remove(string key) + return value; + } + + public bool Set(string key, object? value) + { + // no place to cache so just return the callback result + if (!TryGetContextItems(out IDictionary? items)) { - //no place to cache so just return the callback result - if (!TryGetContextItems(out var items)) - { - return false; - } - - key = GetCacheKey(key); - try - { - - EnterWriteLock(); - items.Remove(key); - } - finally - { - ExitWriteLock(); - } - return true; + return false; } - #region Entries - - protected override IEnumerable> GetDictionaryEntries() + key = GetCacheKey(key); + try { - const string prefix = CacheItemPrefix + "-"; - - if (!TryGetContextItems(out var items)) - return Enumerable.Empty>(); - - return items.Cast>() - .Where(x => x.Key is string s && s.StartsWith(prefix)); + EnterWriteLock(); + items[key] = SafeLazy.GetSafeLazy(() => value); + } + finally + { + ExitWriteLock(); } - protected override void RemoveEntry(string key) - { - if (!TryGetContextItems(out var items)) - return; + return true; + } + public bool Remove(string key) + { + // no place to cache so just return the callback result + if (!TryGetContextItems(out IDictionary? items)) + { + return false; + } + + key = GetCacheKey(key); + try + { + EnterWriteLock(); items.Remove(key); } - - protected override object? GetEntry(string key) + finally { - return !TryGetContextItems(out var items) ? null : items[key]; + ExitWriteLock(); } - #endregion + return true; + } - #region Lock - - protected override void EnterReadLock() + public IEnumerator> GetEnumerator() + { + if (!TryGetContextItems(out IDictionary? items)) { - object? locker = GetLock(); - if (locker == null) - { - return; - } - Monitor.Enter(locker); + yield break; } - protected override void EnterWriteLock() + foreach (KeyValuePair item in items) { - object? locker = GetLock(); - if (locker == null) - { - return; - } - Monitor.Enter(locker); - } - - protected override void ExitReadLock() - { - object? locker = GetLock(); - if (locker == null) - { - return; - } - if (Monitor.IsEntered(locker)) - { - Monitor.Exit(locker); - } - } - - protected override void ExitWriteLock() - { - object? locker = GetLock(); - if (locker == null) - { - return; - } - if (Monitor.IsEntered(locker)) - { - Monitor.Exit(locker); - } - } - - #endregion - - public IEnumerator> GetEnumerator() - { - if (!TryGetContextItems(out IDictionary? items)) - { - yield break; - } - - foreach (KeyValuePair item in items) - { - yield return new KeyValuePair(item.Key.ToString()!, item.Value); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// Ensures and returns the current lock - /// - /// - private object? GetLock() - { - HttpContext? httpContext = _httpContextAccessor.HttpContext; - if (httpContext == null) - { - return null; - } - - RequestLock? requestLock = httpContext.Features.Get(); - if (requestLock != null) - { - return requestLock.SyncRoot; - } - - IFeatureCollection features = httpContext.Features; - - lock (httpContext) - { - requestLock = new RequestLock(); - features.Set(requestLock); - return requestLock.SyncRoot; - } - } - - /// - /// Used as Scoped instance to allow locking within a request - /// - private class RequestLock - { - public object SyncRoot { get; } = new object(); + yield return new KeyValuePair(item.Key.ToString()!, item.Value); } } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private bool TryGetContextItems([MaybeNullWhen(false)] out IDictionary items) + { + items = _httpContextAccessor.HttpContext?.Items; + return items != null; + } + + /// + /// Ensures and returns the current lock + /// + /// + private object? GetLock() + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + return null; + } + + RequestLock? requestLock = httpContext.Features.Get(); + if (requestLock != null) + { + return requestLock.SyncRoot; + } + + IFeatureCollection features = httpContext.Features; + + lock (httpContext) + { + requestLock = new RequestLock(); + features.Set(requestLock); + return requestLock.SyncRoot; + } + } + + /// + /// Used as Scoped instance to allow locking within a request + /// + private class RequestLock + { + public object SyncRoot { get; } = new(); + } + + #region Entries + + protected override IEnumerable> GetDictionaryEntries() + { + const string prefix = CacheItemPrefix + "-"; + + if (!TryGetContextItems(out IDictionary? items)) + { + return Enumerable.Empty>(); + } + + return items + .Where(x => x.Value is not null && x.Key is string s && s.StartsWith(prefix))!; + } + + protected override void RemoveEntry(string key) + { + if (!TryGetContextItems(out IDictionary? items)) + { + return; + } + + items.Remove(key); + } + + protected override object? GetEntry(string key) => + !TryGetContextItems(out IDictionary? items) ? null : items[key]; + + #endregion + + #region Lock + + protected override void EnterReadLock() + { + var locker = GetLock(); + if (locker == null) + { + return; + } + + Monitor.Enter(locker); + } + + protected override void EnterWriteLock() + { + var locker = GetLock(); + if (locker == null) + { + return; + } + + Monitor.Enter(locker); + } + + protected override void ExitReadLock() + { + var locker = GetLock(); + if (locker == null) + { + return; + } + + if (Monitor.IsEntered(locker)) + { + Monitor.Exit(locker); + } + } + + protected override void ExitWriteLock() + { + var locker = GetLock(); + if (locker == null) + { + return; + } + + if (Monitor.IsEntered(locker)) + { + Monitor.Exit(locker); + } + } + + #endregion } diff --git a/src/Umbraco.Web.Common/Constants/ViewConstants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs index 5c8ec4974a..2844db3daf 100644 --- a/src/Umbraco.Web.Common/Constants/ViewConstants.cs +++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Web.Common.Constants +namespace Umbraco.Cms.Web.Common.Constants; + +/// +/// constants +/// +internal static class ViewConstants { - /// - /// constants - /// - internal static class ViewConstants + internal const string ViewLocation = "~/Views"; + + internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + + internal static class ReservedAdditionalKeys { - internal const string ViewLocation = "~/Views"; - - internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; - - internal static class ReservedAdditionalKeys - { - internal const string Controller = "c"; - internal const string Action = "a"; - internal const string Area = "ar"; - } + internal const string Controller = "c"; + internal const string Action = "a"; + internal const string Area = "ar"; } } diff --git a/src/Umbraco.Web.Common/Controllers/IRenderController.cs b/src/Umbraco.Web.Common/Controllers/IRenderController.cs index 21a5eda83a..d7212e5586 100644 --- a/src/Umbraco.Web.Common/Controllers/IRenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/IRenderController.cs @@ -1,12 +1,11 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Web.Common.Controllers -{ - /// - /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking - /// - public interface IRenderController : IDiscoverable - { +namespace Umbraco.Cms.Web.Common.Controllers; - } +/// +/// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route +/// hijacking +/// +public interface IRenderController : IDiscoverable +{ } diff --git a/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs b/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs index f0e6bdbe6a..616611a224 100644 --- a/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs +++ b/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs @@ -1,16 +1,15 @@ using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Used for custom routed controllers to execute within the context of Umbraco +/// +public interface IVirtualPageController { /// - /// Used for custom routed controllers to execute within the context of Umbraco + /// Returns the to use as the current page for the request /// - public interface IVirtualPageController - { - /// - /// Returns the to use as the current page for the request - /// - IPublishedContent FindContent(ActionExecutingContext actionExecutingContext); - } + IPublishedContent FindContent(ActionExecutingContext actionExecutingContext); } diff --git a/src/Umbraco.Web.Common/Controllers/PluginController.cs b/src/Umbraco.Web.Common/Controllers/PluginController.cs index b1006455ea..59c8457be5 100644 --- a/src/Umbraco.Web.Common/Controllers/PluginController.cs +++ b/src/Umbraco.Web.Common/Controllers/PluginController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; @@ -11,92 +10,94 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for plugin controllers. +/// +public abstract class PluginController : Controller, IDiscoverable { - /// - /// Provides a base class for plugin controllers. - /// - public abstract class PluginController : Controller, IDiscoverable + private static readonly ConcurrentDictionary MetadataStorage = new(); + + protected PluginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger) { - private static readonly ConcurrentDictionary MetadataStorage - = new ConcurrentDictionary(); + UmbracoContextAccessor = umbracoContextAccessor; + DatabaseFactory = databaseFactory; + Services = services; + AppCaches = appCaches; + ProfilingLogger = profilingLogger; + } - // for debugging purposes - internal Guid InstanceId { get; } = Guid.NewGuid(); - - /// - /// Gets the Umbraco context. - /// - public virtual IUmbracoContext UmbracoContext + /// + /// Gets the Umbraco context. + /// + public virtual IUmbracoContext UmbracoContext + { + get { - get - { - var umbracoContext = UmbracoContextAccessor.GetRequiredUmbracoContext(); - return umbracoContext; - } - } - - /// - /// Gets the database context accessor. - /// - public virtual IUmbracoContextAccessor UmbracoContextAccessor { get; } - - /// - /// Gets the database context. - /// - public IUmbracoDatabaseFactory DatabaseFactory { get; } - - /// - /// Gets or sets the services context. - /// - public ServiceContext Services { get; } - - /// - /// Gets or sets the application cache. - /// - public AppCaches AppCaches { get; } - - /// - /// Gets or sets the profiling logger. - /// - public IProfilingLogger ProfilingLogger { get; } - - /// - /// Gets metadata for this instance. - /// - internal PluginControllerMetadata Metadata => GetMetadata(GetType()); - - protected PluginController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger) - { - UmbracoContextAccessor = umbracoContextAccessor; - DatabaseFactory = databaseFactory; - Services = services; - AppCaches = appCaches; - ProfilingLogger = profilingLogger; - } - - /// - /// Gets metadata for a controller type. - /// - /// The controller type. - /// Metadata for the controller type. - public static PluginControllerMetadata GetMetadata(Type controllerType) - { - return MetadataStorage.GetOrAdd(controllerType, type => - { - // plugin controller? back-office controller? - var pluginAttribute = controllerType.GetCustomAttribute(false); - var backOfficeAttribute = controllerType.GetCustomAttribute(true); - - return new PluginControllerMetadata - { - AreaName = pluginAttribute?.AreaName, - ControllerName = ControllerExtensions.GetControllerName(controllerType), - ControllerNamespace = controllerType.Namespace, - ControllerType = controllerType, - IsBackOffice = backOfficeAttribute != null - }; - }); + IUmbracoContext umbracoContext = UmbracoContextAccessor.GetRequiredUmbracoContext(); + return umbracoContext; } } + + /// + /// Gets the database context accessor. + /// + public virtual IUmbracoContextAccessor UmbracoContextAccessor { get; } + + /// + /// Gets the database context. + /// + public IUmbracoDatabaseFactory DatabaseFactory { get; } + + /// + /// Gets or sets the services context. + /// + public ServiceContext Services { get; } + + /// + /// Gets or sets the application cache. + /// + public AppCaches AppCaches { get; } + + /// + /// Gets or sets the profiling logger. + /// + public IProfilingLogger ProfilingLogger { get; } + + /// + /// Gets metadata for this instance. + /// + internal PluginControllerMetadata Metadata => GetMetadata(GetType()); + + // for debugging purposes + internal Guid InstanceId { get; } = Guid.NewGuid(); + + /// + /// Gets metadata for a controller type. + /// + /// The controller type. + /// Metadata for the controller type. + public static PluginControllerMetadata GetMetadata(Type controllerType) => + MetadataStorage.GetOrAdd(controllerType, type => + { + // plugin controller? back-office controller? + PluginControllerAttribute? pluginAttribute = + controllerType.GetCustomAttribute(false); + IsBackOfficeAttribute? backOfficeAttribute = controllerType.GetCustomAttribute(true); + + return new PluginControllerMetadata + { + AreaName = pluginAttribute?.AreaName, + ControllerName = ControllerExtensions.GetControllerName(controllerType), + ControllerNamespace = controllerType.Namespace, + ControllerType = controllerType, + IsBackOffice = backOfficeAttribute != null, + }; + }); } diff --git a/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs b/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs index e44f3270fe..636811e454 100644 --- a/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs +++ b/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs @@ -1,29 +1,28 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// A request feature to allowing proxying viewdata from one controller to another +/// +public sealed class ProxyViewDataFeature { /// - /// A request feature to allowing proxying viewdata from one controller to another + /// Initializes a new instance of the class. /// - public sealed class ProxyViewDataFeature + public ProxyViewDataFeature(ViewDataDictionary viewData, ITempDataDictionary tempData) { - /// - /// Initializes a new instance of the class. - /// - public ProxyViewDataFeature(ViewDataDictionary viewData, ITempDataDictionary tempData) - { - ViewData = viewData; - TempData = tempData; - } - - /// - /// Gets the - /// - public ViewDataDictionary ViewData { get; } - - /// - /// Gets the - /// - public ITempDataDictionary TempData { get; } + ViewData = viewData; + TempData = tempData; } + + /// + /// Gets the + /// + public ViewDataDictionary ViewData { get; } + + /// + /// Gets the + /// + public ITempDataDictionary TempData { get; } } diff --git a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs index 4274a1ab16..95b8bc7320 100644 --- a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs @@ -1,78 +1,76 @@ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Deals with custom headers for the umbraco request +/// +internal class PublishedRequestFilterAttribute : ResultFilterAttribute { /// - /// Deals with custom headers for the umbraco request + /// Deals with custom headers for the umbraco request /// - internal class PublishedRequestFilterAttribute : ResultFilterAttribute + public override void OnResultExecuting(ResultExecutingContext context) { - /// - /// Gets the - /// - protected UmbracoRouteValues GetUmbracoRouteValues(ResultExecutingContext context) - { - UmbracoRouteValues? routeVals = context.HttpContext.Features.Get(); - if (routeVals == null) - { - throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); - } + UmbracoRouteValues routeVals = GetUmbracoRouteValues(context); + IPublishedRequest pcr = routeVals.PublishedRequest; - return routeVals; + // now we can deal with headers, etc... + if (pcr.ResponseStatusCode.HasValue) + { + // set status code -- even for redirects + context.HttpContext.Response.StatusCode = pcr.ResponseStatusCode.Value; } - /// - /// Deals with custom headers for the umbraco request - /// - public override void OnResultExecuting(ResultExecutingContext context) + AddCacheControlHeaders(context, pcr); + + if (pcr.Headers != null) { - UmbracoRouteValues routeVals = GetUmbracoRouteValues(context); - IPublishedRequest pcr = routeVals.PublishedRequest; - - // now we can deal with headers, etc... - if (pcr.ResponseStatusCode.HasValue) + foreach (KeyValuePair header in pcr.Headers) { - // set status code -- even for redirects - context.HttpContext.Response.StatusCode = pcr.ResponseStatusCode.Value; - } - - AddCacheControlHeaders(context, pcr); - - if (pcr.Headers != null) - { - foreach (KeyValuePair header in pcr.Headers) - { - context.HttpContext.Response.Headers.Append(header.Key, header.Value); - } - } - } - - private void AddCacheControlHeaders(ResultExecutingContext context, IPublishedRequest pcr) - { - var cacheControlHeaders = new List(); - - if (pcr.SetNoCacheHeader) - { - cacheControlHeaders.Add("no-cache"); - } - - if (pcr.CacheExtensions != null) - { - foreach (var cacheExtension in pcr.CacheExtensions) - { - cacheControlHeaders.Add(cacheExtension); - } - } - - if (cacheControlHeaders.Count > 0) - { - context.HttpContext.Response.Headers["Cache-Control"] = string.Join(", ", cacheControlHeaders); + context.HttpContext.Response.Headers.Append(header.Key, header.Value); } } } + + /// + /// Gets the + /// + protected UmbracoRouteValues GetUmbracoRouteValues(ResultExecutingContext context) + { + UmbracoRouteValues? routeVals = context.HttpContext.Features.Get(); + if (routeVals == null) + { + throw new InvalidOperationException( + $"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); + } + + return routeVals; + } + + private void AddCacheControlHeaders(ResultExecutingContext context, IPublishedRequest pcr) + { + var cacheControlHeaders = new List(); + + if (pcr.SetNoCacheHeader) + { + cacheControlHeaders.Add("no-cache"); + } + + if (pcr.CacheExtensions != null) + { + foreach (var cacheExtension in pcr.CacheExtensions) + { + cacheControlHeaders.Add(cacheExtension); + } + } + + if (cacheControlHeaders.Count > 0) + { + context.HttpContext.Response.Headers["Cache-Control"] = string.Join(", ", cacheControlHeaders); + } + } } diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 46d3d4119d..a9771c0ab8 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; @@ -13,136 +9,133 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Represents the default front-end rendering controller. +/// +[ModelBindingException] +[PublishedRequestFilter] +public class RenderController : UmbracoPageController, IRenderController { + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; /// - /// Represents the default front-end rendering controller. + /// Initializes a new instance of the class. /// - [ModelBindingException] - [PublishedRequestFilter] - public class RenderController : UmbracoPageController, IRenderController + public RenderController(ILogger logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor) + : base(logger, compositeViewEngine) { - private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + _logger = logger; + _umbracoContextAccessor = umbracoContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public RenderController(ILogger logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor) - : base(logger, compositeViewEngine) + /// + /// Gets the umbraco context + /// + protected IUmbracoContext UmbracoContext + { + get { - _logger = logger; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Gets the umbraco context - /// - protected IUmbracoContext UmbracoContext - { - get - { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - return umbracoContext; - } - } - - /// - /// The default action to render the front-end view. - /// - public virtual IActionResult Index() => CurrentTemplate(new ContentModel(CurrentPage)); - - /// - /// Gets an action result based on the template name found in the route values and a model. - /// - /// The type of the model. - /// The model. - /// The action result. - /// - /// If the template found in the route values doesn't physically exist, Umbraco not found result is returned. - /// - protected override IActionResult CurrentTemplate(T model) - { - if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) - { - // no physical template file was found - return new PublishedContentNotFoundResult(UmbracoContext); - } - - return View(UmbracoRouteValues.TemplateName, model); - } - - /// - /// Before the controller executes we will handle redirects and not founds - /// - public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - IPublishedRequest pcr = UmbracoRouteValues.PublishedRequest; - - _logger.LogDebug( - "Response status: Content={Content}, StatusCode={ResponseStatusCode}, Culture={Culture}", - pcr.PublishedContent?.Id ?? -1, - pcr.ResponseStatusCode, - pcr.Culture); - - UmbracoRouteResult routeStatus = pcr.GetRouteResult(); - switch (routeStatus) - { - case UmbracoRouteResult.Redirect: - - // set the redirect result and do not call next to short circuit - context.Result = pcr.IsRedirectPermanent() - ? RedirectPermanent(pcr.RedirectUrl!) - : Redirect(pcr.RedirectUrl!); - break; - case UmbracoRouteResult.NotFound: - // set the redirect result and do not call next to short circuit - context.Result = GetNoTemplateResult(pcr); - break; - case UmbracoRouteResult.Success: - default: - - // Check if there's a ProxyViewDataFeature in the request. - // If there it is means that we are proxying/executing this controller - // from another controller and we need to merge it's ViewData with this one - // since this one will be empty. - ProxyViewDataFeature? saveViewData = HttpContext.Features.Get(); - if (saveViewData != null) - { - foreach (KeyValuePair kv in saveViewData.ViewData) - { - ViewData[kv.Key] = kv.Value; - } - } - - // continue normally - await next(); - break; - } - } - - private PublishedContentNotFoundResult GetNoTemplateResult(IPublishedRequest pcr) - { - // missing template, so we're in a 404 here - // so the content, if any, is a custom 404 page of some sort - if (!pcr.HasPublishedContent()) - { - // means the builder could not find a proper document to handle 404 - return new PublishedContentNotFoundResult(UmbracoContext); - } - else if (!pcr.HasTemplate()) - { - // means the engine could find a proper document, but the document has no template - // at that point there isn't much we can do - return new PublishedContentNotFoundResult( - UmbracoContext, - "In addition, no template exists to render the custom 404."); - } - else - { - return new PublishedContentNotFoundResult(UmbracoContext); - } + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + return umbracoContext; } } + + /// + /// The default action to render the front-end view. + /// + public virtual IActionResult Index() => CurrentTemplate(new ContentModel(CurrentPage)); + + /// + /// Before the controller executes we will handle redirects and not founds + /// + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + IPublishedRequest pcr = UmbracoRouteValues.PublishedRequest; + + _logger.LogDebug( + "Response status: Content={Content}, StatusCode={ResponseStatusCode}, Culture={Culture}", + pcr.PublishedContent?.Id ?? -1, + pcr.ResponseStatusCode, + pcr.Culture); + + UmbracoRouteResult routeStatus = pcr.GetRouteResult(); + switch (routeStatus) + { + case UmbracoRouteResult.Redirect: + + // set the redirect result and do not call next to short circuit + context.Result = pcr.IsRedirectPermanent() + ? RedirectPermanent(pcr.RedirectUrl!) + : Redirect(pcr.RedirectUrl!); + break; + case UmbracoRouteResult.NotFound: + // set the redirect result and do not call next to short circuit + context.Result = GetNoTemplateResult(pcr); + break; + case UmbracoRouteResult.Success: + default: + + // Check if there's a ProxyViewDataFeature in the request. + // If there it is means that we are proxying/executing this controller + // from another controller and we need to merge it's ViewData with this one + // since this one will be empty. + ProxyViewDataFeature? saveViewData = HttpContext.Features.Get(); + if (saveViewData != null) + { + foreach (KeyValuePair kv in saveViewData.ViewData) + { + ViewData[kv.Key] = kv.Value; + } + } + + // continue normally + await next(); + break; + } + } + + /// + /// Gets an action result based on the template name found in the route values and a model. + /// + /// The type of the model. + /// The model. + /// The action result. + /// + /// If the template found in the route values doesn't physically exist, Umbraco not found result is returned. + /// + protected override IActionResult CurrentTemplate(T model) + { + if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) + { + // no physical template file was found + return new PublishedContentNotFoundResult(UmbracoContext); + } + + return View(UmbracoRouteValues.TemplateName, model); + } + + private PublishedContentNotFoundResult GetNoTemplateResult(IPublishedRequest pcr) + { + // missing template, so we're in a 404 here + // so the content, if any, is a custom 404 page of some sort + if (!pcr.HasPublishedContent()) + { + // means the builder could not find a proper document to handle 404 + return new PublishedContentNotFoundResult(UmbracoContext); + } + + if (!pcr.HasTemplate()) + { + // means the engine could find a proper document, but the document has no template + // at that point there isn't much we can do + return new PublishedContentNotFoundResult( + UmbracoContext, + "In addition, no template exists to render the custom 404."); + } + + return new PublishedContentNotFoundResult(UmbracoContext); + } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs index f00f2fec57..05d7004e1f 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -1,17 +1,16 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for auto-routed Umbraco API controllers. +/// +public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { /// - /// Provides a base class for auto-routed Umbraco API controllers. + /// Initializes a new instance of the class. /// - public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable + protected UmbracoApiController() { - /// - /// Initializes a new instance of the class. - /// - protected UmbracoApiController() - { - } } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 8dfd5a76af..e5cbe66cf6 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -4,24 +4,23 @@ using Umbraco.Cms.Core.Features; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for Umbraco API controllers. +/// +/// +/// These controllers are NOT auto-routed. +/// The base class is which are netcore API controllers without any view support +/// +[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] // TODO: This could be part of our conventions +[UmbracoApiController] +public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { /// - /// Provides a base class for Umbraco API controllers. + /// Initializes a new instance of the class. /// - /// - /// These controllers are NOT auto-routed. - /// The base class is which are netcore API controllers without any view support - /// - [Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] // TODO: This could be part of our conventions - [UmbracoApiController] - public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature + protected UmbracoApiControllerBase() { - /// - /// Initializes a new instance of the class. - /// - protected UmbracoApiControllerBase() - { - } } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs index 9f0c353092..87a8c8e56d 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs @@ -1,12 +1,11 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Web.Common.Controllers -{ - public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase - { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK +namespace Umbraco.Cms.Web.Common.Controllers; - protected override UmbracoApiControllerTypeCollectionBuilder This => this; - } +public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase< + UmbracoApiControllerTypeCollectionBuilder, UmbracoApiControllerTypeCollection, UmbracoApiController> +{ + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + protected override UmbracoApiControllerTypeCollectionBuilder This => this; } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoAuthorizedController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoAuthorizedController.cs index b949eef8bf..7d6563b996 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoAuthorizedController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoAuthorizedController.cs @@ -3,14 +3,13 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Filters; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for backoffice authorized controllers. +/// +[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] +[DisableBrowserCache] +public abstract class UmbracoAuthorizedController : ControllerBase { - /// - /// Provides a base class for backoffice authorized controllers. - /// - [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] - [DisableBrowserCache] - public abstract class UmbracoAuthorizedController : ControllerBase - { - } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs index 3d714e8e60..9342171cbd 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs @@ -1,15 +1,12 @@ -using System; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Cms.Web.Common.Controllers -{ - /// - /// Provides a base class for Umbraco controllers. - /// - public abstract class UmbracoController : Controller - { - // for debugging purposes - internal Guid InstanceId { get; } = Guid.NewGuid(); +namespace Umbraco.Cms.Web.Common.Controllers; - } +/// +/// Provides a base class for Umbraco controllers. +/// +public abstract class UmbracoController : Controller +{ + // for debugging purposes + internal Guid InstanceId { get; } = Guid.NewGuid(); } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs index e10e3b5217..4634f4e48a 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; @@ -6,105 +5,111 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Common.Controllers +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// An abstract controller for a front-end Umbraco page +/// +public abstract class UmbracoPageController : UmbracoController { + private readonly ICompositeViewEngine _compositeViewEngine; + private readonly ILogger _logger; + private UmbracoRouteValues? _umbracoRouteValues; + /// - /// An abstract controller for a front-end Umbraco page + /// Initializes a new instance of the class. /// - public abstract class UmbracoPageController : UmbracoController + protected UmbracoPageController(ILogger logger, ICompositeViewEngine compositeViewEngine) { - private UmbracoRouteValues? _umbracoRouteValues; - private readonly ICompositeViewEngine _compositeViewEngine; - private readonly ILogger _logger; + _logger = logger; + _compositeViewEngine = compositeViewEngine; + } - /// - /// Initializes a new instance of the class. - /// - protected UmbracoPageController(ILogger logger, ICompositeViewEngine compositeViewEngine) + /// + /// Gets the + /// + protected virtual UmbracoRouteValues UmbracoRouteValues + { + get { - _logger = logger; - _compositeViewEngine = compositeViewEngine; - } - - /// - /// Gets the - /// - protected virtual UmbracoRouteValues UmbracoRouteValues - { - get + if (_umbracoRouteValues != null) { - if (_umbracoRouteValues != null) - { - return _umbracoRouteValues; - } - - _umbracoRouteValues = HttpContext.Features.Get(); - - if (_umbracoRouteValues == null) - { - throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); - } - return _umbracoRouteValues; } - } - /// - /// Gets the current content item. - /// - protected virtual IPublishedContent? CurrentPage - { - get + _umbracoRouteValues = HttpContext.Features.Get(); + + if (_umbracoRouteValues == null) { - if (!UmbracoRouteValues.PublishedRequest.HasPublishedContent()) - { - // This will never be accessed this way since the controller will handle redirects and not founds - // before this can be accessed but we need to be explicit. - throw new InvalidOperationException("There is no published content found in the request"); - } - - return UmbracoRouteValues.PublishedRequest.PublishedContent; - } - } - - /// - /// Gets an action result based on the template name found in the route values and a model. - /// - /// The type of the model. - /// The model. - /// The action result. - /// If the template found in the route values doesn't physically exist and exception is thrown - protected virtual IActionResult CurrentTemplate(T model) - { - if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) - { - throw new InvalidOperationException("No physical template file was found for template " + UmbracoRouteValues.TemplateName); + throw new InvalidOperationException( + $"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); } - return View(UmbracoRouteValues.TemplateName, model); - } - - /// - /// Ensures that a physical view file exists on disk. - /// - /// The view name. - protected bool EnsurePhsyicalViewExists(string? template) - { - if (string.IsNullOrWhiteSpace(template)) - { - string? docTypeAlias = UmbracoRouteValues.PublishedRequest.PublishedContent?.ContentType.Alias; - _logger.LogWarning("No physical template file was found for document type with alias {Alias}", docTypeAlias); - return false; - } - - ViewEngineResult result = _compositeViewEngine.FindView(ControllerContext, template, false); - if (result.View != null) - { - return true; - } - - _logger.LogWarning("No physical template file was found for template {Template}", template); - return false; + return _umbracoRouteValues; } } + + /// + /// Gets the current content item. + /// + protected virtual IPublishedContent? CurrentPage + { + get + { + if (!UmbracoRouteValues.PublishedRequest.HasPublishedContent()) + { + // This will never be accessed this way since the controller will handle redirects and not founds + // before this can be accessed but we need to be explicit. + throw new InvalidOperationException("There is no published content found in the request"); + } + + return UmbracoRouteValues.PublishedRequest.PublishedContent; + } + } + + /// + /// Gets an action result based on the template name found in the route values and a model. + /// + /// The type of the model. + /// The model. + /// The action result. + /// + /// If the template found in the route values doesn't physically exist and + /// exception is thrown + /// + protected virtual IActionResult CurrentTemplate(T model) + { + if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) + { + throw new InvalidOperationException("No physical template file was found for template " + + UmbracoRouteValues.TemplateName); + } + + return View(UmbracoRouteValues.TemplateName, model); + } + + /// + /// Ensures that a physical view file exists on disk. + /// + /// The view name. + protected bool EnsurePhsyicalViewExists(string? template) + { + if (string.IsNullOrWhiteSpace(template)) + { + var docTypeAlias = UmbracoRouteValues.PublishedRequest.PublishedContent?.ContentType.Alias; + _logger.LogWarning( + "No physical template file was found for document type with alias {Alias}", + docTypeAlias); + return false; + } + + ViewEngineResult result = _compositeViewEngine.FindView(ControllerContext, template, false); + if (result.View != null) + { + return true; + } + + _logger.LogWarning("No physical template file was found for template {Template}", template); + return false; + } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index 69b37cd7da..2b372992cc 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Options; @@ -10,79 +8,80 @@ using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.DependencyInjection +namespace Umbraco.Cms.Web.Common.DependencyInjection; + +/// +/// Configures the ImageSharp middleware options. +/// +/// +public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions { + private readonly Configuration _configuration; + private readonly ImagingSettings _imagingSettings; + /// - /// Configures the ImageSharp middleware options. + /// Initializes a new instance of the class. /// - /// - public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions + /// The ImageSharp configuration. + /// The Umbraco imaging settings. + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) { - private readonly Configuration _configuration; - private readonly ImagingSettings _imagingSettings; + _configuration = configuration; + _imagingSettings = imagingSettings.Value; + } - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - /// The Umbraco imaging settings. - public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) + /// + public void Configure(ImageSharpMiddlewareOptions options) + { + options.Configuration = _configuration; + + options.BrowserMaxAge = _imagingSettings.Cache.BrowserMaxAge; + options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; + options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; + + // Use configurable maximum width and height + options.OnParseCommandsAsync = context => { - _configuration = configuration; - _imagingSettings = imagingSettings.Value; - } + if (context.Commands.Count == 0) + { + return Task.CompletedTask; + } - /// - public void Configure(ImageSharpMiddlewareOptions options) + var width = context.Parser.ParseValue( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), + context.Culture); + if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) + { + context.Commands.Remove(ResizeWebProcessor.Width); + } + + var height = context.Parser.ParseValue( + context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), + context.Culture); + if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) + { + context.Commands.Remove(ResizeWebProcessor.Height); + } + + return Task.CompletedTask; + }; + + // Change Cache-Control header when cache buster value is present + options.OnPrepareResponseAsync = context => { - options.Configuration = _configuration; - - options.BrowserMaxAge = _imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; - options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; - - // Use configurable maximum width and height - options.OnParseCommandsAsync = context => + if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) { - if (context.Commands.Count == 0) - { - return Task.CompletedTask; - } + ResponseHeaders headers = context.Response.GetTypedHeaders(); - int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) - { - context.Commands.Remove(ResizeWebProcessor.Width); - } + CacheControlHeaderValue cacheControl = + headers.CacheControl ?? new CacheControlHeaderValue { Public = true }; + cacheControl.MustRevalidate = false; // ImageSharp enables this by default + cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); - int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) - { - context.Commands.Remove(ResizeWebProcessor.Height); - } + headers.CacheControl = cacheControl; + } - return Task.CompletedTask; - }; - - // Change Cache-Control header when cache buster value is present - options.OnPrepareResponseAsync = context => - { - if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) - { - ResponseHeaders headers = context.Response.GetTypedHeaders(); - - CacheControlHeaderValue cacheControl = headers.CacheControl ?? new CacheControlHeaderValue() - { - Public = true - }; - cacheControl.MustRevalidate = false; // ImageSharp enables this by default - cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); - - headers.CacheControl = cacheControl; - } - - return Task.CompletedTask; - }; - } + return Task.CompletedTask; + }; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs index 16f2476189..4f4b337021 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs @@ -4,33 +4,34 @@ using SixLabors.ImageSharp.Web.Caching; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; -namespace Umbraco.Cms.Web.Common.DependencyInjection +namespace Umbraco.Cms.Web.Common.DependencyInjection; + +/// +/// Configures the ImageSharp physical file system cache options. +/// +/// +public sealed class ConfigurePhysicalFileSystemCacheOptions : IConfigureOptions { + private readonly IHostEnvironment _hostEnvironment; + private readonly ImagingSettings _imagingSettings; + /// - /// Configures the ImageSharp physical file system cache options. + /// Initializes a new instance of the class. /// - /// - public sealed class ConfigurePhysicalFileSystemCacheOptions : IConfigureOptions + /// The Umbraco imaging settings. + /// The host environment. + public ConfigurePhysicalFileSystemCacheOptions( + IOptions imagingSettings, + IHostEnvironment hostEnvironment) { - private readonly ImagingSettings _imagingSettings; - private readonly IHostEnvironment _hostEnvironment; + _imagingSettings = imagingSettings.Value; + _hostEnvironment = hostEnvironment; + } - /// - /// Initializes a new instance of the class. - /// - /// The Umbraco imaging settings. - /// The host environment. - public ConfigurePhysicalFileSystemCacheOptions(IOptions imagingSettings, IHostEnvironment hostEnvironment) - { - _imagingSettings = imagingSettings.Value; - _hostEnvironment = hostEnvironment; - } - - /// - public void Configure(PhysicalFileSystemCacheOptions options) - { - options.CacheFolder = _hostEnvironment.MapPathContentRoot(_imagingSettings.Cache.CacheFolder); - options.CacheFolderDepth = _imagingSettings.Cache.CacheFolderDepth; - } + /// + public void Configure(PhysicalFileSystemCacheOptions options) + { + options.CacheFolder = _hostEnvironment.MapPathContentRoot(_imagingSettings.Cache.CacheFolder); + options.CacheFolderDepth = _imagingSettings.Cache.CacheFolderDepth; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs b/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs index 5c9cf017af..31f5709bd4 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs @@ -1,17 +1,15 @@ -using System; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Web.Common.DependencyInjection +namespace Umbraco.Cms.Web.Common.DependencyInjection; + +/// +internal class ScopedServiceProvider : IScopedServiceProvider { + private readonly IHttpContextAccessor _accessor; + + public ScopedServiceProvider(IHttpContextAccessor accessor) => _accessor = accessor; + /// - internal class ScopedServiceProvider : IScopedServiceProvider - { - private readonly IHttpContextAccessor _accessor; - - public ScopedServiceProvider(IHttpContextAccessor accessor) => _accessor = accessor; - - /// - public IServiceProvider? ServiceProvider => _accessor.HttpContext?.RequestServices; - } + public IServiceProvider? ServiceProvider => _accessor.HttpContext?.RequestServices; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index cfba33d0ae..e5e642f4a3 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -10,31 +10,34 @@ using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; using Umbraco.Cms.Web.Common.Media; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds Image Sharp with Umbraco settings + /// + public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { - /// - /// Adds Image Sharp with Umbraco settings - /// - public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddImageSharp() - // Replace default image provider - .ClearProviders() - .AddProvider() - // Add custom processors - .AddProcessor(); + builder.Services.AddImageSharp() - // Configure middleware - builder.Services.AddTransient, ConfigureImageSharpMiddlewareOptions>(); + // Replace default image provider + .ClearProviders() + .AddProvider() - // Configure cache options - builder.Services.AddTransient, ConfigurePhysicalFileSystemCacheOptions>(); + // Add custom processors + .AddProcessor(); - return builder.Services; - } + // Configure middleware + builder.Services + .AddTransient, ConfigureImageSharpMiddlewareOptions>(); + + // Configure cache options + builder.Services + .AddTransient, ConfigurePhysicalFileSystemCacheOptions>(); + + return builder.Services; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index 7ad9618ba5..123e39f5e2 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -1,10 +1,5 @@ -using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Notifications; @@ -15,66 +10,63 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Security; using Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds Identity support for Umbraco members + /// + public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) { - /// - /// Adds Identity support for Umbraco members - /// - public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) + IServiceCollection services = builder.Services; + + // check if this has already been added, we cannot add twice but both front-end and back end + // depend on this so it's possible it can be called twice. + var distCacheBinder = + new UniqueServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped); + if (builder.Services.Contains(distCacheBinder)) { - IServiceCollection services = builder.Services; - - // check if this has already been added, we cannot add twice but both front-end and back end - // depend on this so it's possible it can be called twice. - var distCacheBinder = new UniqueServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped); - if (builder.Services.Contains(distCacheBinder)) - { - return builder; - } - - // NOTE: We are using AddIdentity which is going to add all of the default AuthN/AuthZ configurations = OK! - // This will also add all of the default identity services for our user/role types that we aren't overriding = OK! - // If a developer wishes to use Umbraco Members with different AuthN/AuthZ values, like different cookie values - // or authentication scheme's then they can call the default identity configuration methods like ConfigureApplicationCookie. - // BUT ... if a developer wishes to use the default auth schemes for entirely separate purposes alongside Umbraco members, - // then we'll probably have to change this and make it more flexible like how we do for Users. Which means booting up - // identity here with the basics and registering all of our own custom services. - // Since we are using the defaults in v8 (and below) for members, I think using the default for members now is OK! - - services.AddIdentity() - .AddDefaultTokenProviders() - .AddUserStore, MemberUserStore>(factory => new MemberUserStore( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService() - )) - .AddRoleStore() - .AddRoleManager() - .AddMemberManager() - .AddSignInManager() - .AddErrorDescriber() - .AddUserConfirmation>(); - - - builder.AddNotificationHandler(); - builder.AddNotificationAsyncHandler(); - services.ConfigureOptions(); - - services.AddScoped(x => (IMemberUserStore)x.GetRequiredService>()); - services.AddScoped, MemberPasswordHasher>(); - - services.ConfigureOptions(); - services.ConfigureOptions(); - - services.AddUnique(); - return builder; } + + // NOTE: We are using AddIdentity which is going to add all of the default AuthN/AuthZ configurations = OK! + // This will also add all of the default identity services for our user/role types that we aren't overriding = OK! + // If a developer wishes to use Umbraco Members with different AuthN/AuthZ values, like different cookie values + // or authentication scheme's then they can call the default identity configuration methods like ConfigureApplicationCookie. + // BUT ... if a developer wishes to use the default auth schemes for entirely separate purposes alongside Umbraco members, + // then we'll probably have to change this and make it more flexible like how we do for Users. Which means booting up + // identity here with the basics and registering all of our own custom services. + // Since we are using the defaults in v8 (and below) for members, I think using the default for members now is OK! + services.AddIdentity() + .AddDefaultTokenProviders() + .AddUserStore, MemberUserStore>(factory => new MemberUserStore( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService())) + .AddRoleStore() + .AddRoleManager() + .AddMemberManager() + .AddSignInManager() + .AddErrorDescriber() + .AddUserConfirmation>(); + + builder.AddNotificationHandler(); + builder.AddNotificationAsyncHandler(); + services.ConfigureOptions(); + + services.AddScoped(x => (IMemberUserStore)x.GetRequiredService>()); + services.AddScoped, MemberPasswordHasher>(); + + services.ConfigureOptions(); + services.ConfigureOptions(); + + services.AddUnique(); + + return builder; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index c98cb38e82..5a889d423d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,7 +1,4 @@ -using System; using System.Data.Common; -using System.Linq; -using System.Net.Http; using System.Reflection; using Dazinator.Extensions.FileProviders.GlobPatternFilter; using Microsoft.AspNetCore.Builder; @@ -15,8 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -using Serilog; -using Serilog.Extensions.Hosting; +using Microsoft.Extensions.Options; using Serilog.Extensions.Logging; using Smidge; using Smidge.Cache; @@ -64,362 +60,391 @@ using Umbraco.Cms.Web.Common.Templates; using Umbraco.Cms.Web.Common.UmbracoContext; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +// TODO: We could add parameters to configure each of these for flexibility + +/// +/// Extension methods for for the common Umbraco functionality +/// +public static partial class UmbracoBuilderExtensions { - // TODO: We could add parameters to configure each of these for flexibility + /// + /// Creates an and registers basic Umbraco services + /// + public static IUmbracoBuilder AddUmbraco( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IConfiguration config) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (config is null) + { + throw new ArgumentNullException(nameof(config)); + } + + // Setup static application logging ASAP (e.g. during configure services). + // Will log to SilentLogger until Serilog.Log.Logger is setup. + StaticApplicationLogging.Initialize(new SerilogLoggerFactory()); + + // The DataDirectory is used to resolve database file paths (directly supported by SQL CE and manually replaced for LocalDB) + AppDomain.CurrentDomain.SetData( + "DataDirectory", + webHostEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); + + // Manually create and register the HttpContextAccessor. In theory this should not be registered + // again by the user but if that is the case it's not the end of the world since HttpContextAccessor + // is just based on AsyncLocal, see https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/HttpContextAccessor.cs + IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); + services.AddSingleton(httpContextAccessor); + + var requestCache = new HttpContextRequestAppCache(httpContextAccessor); + var appCaches = AppCaches.Create(requestCache); + + services.ConfigureOptions(); + services.ConfigureOptions(); + + IProfiler profiler = GetWebProfiler(config); + + services.AddLogger(webHostEnvironment, config); + + ILoggerFactory loggerFactory = new SerilogLoggerFactory(); + + TypeLoader typeLoader = services.AddTypeLoader(Assembly.GetEntryAssembly(), loggerFactory, config); + + IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config); + return new UmbracoBuilder(services, config, typeLoader, loggerFactory, profiler, appCaches, tempHostingEnvironment); + } /// - /// Extension methods for for the common Umbraco functionality + /// Adds core Umbraco services /// - public static partial class UmbracoBuilderExtensions + /// + /// This will not add any composers/components + /// + public static IUmbracoBuilder AddUmbracoCore(this IUmbracoBuilder builder) { - /// - /// Creates an and registers basic Umbraco services - /// - public static IUmbracoBuilder AddUmbraco( - this IServiceCollection services, - IWebHostEnvironment webHostEnvironment, - IConfiguration config) + if (builder is null) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (config is null) - { - throw new ArgumentNullException(nameof(config)); - } - - // Setup static application logging ASAP (e.g. during configure services). - // Will log to SilentLogger until Serilog.Log.Logger is setup. - StaticApplicationLogging.Initialize(new SerilogLoggerFactory()); - - // The DataDirectory is used to resolve database file paths (directly supported by SQL CE and manually replaced for LocalDB) - AppDomain.CurrentDomain.SetData("DataDirectory", webHostEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - - // Manually create and register the HttpContextAccessor. In theory this should not be registered - // again by the user but if that is the case it's not the end of the world since HttpContextAccessor - // is just based on AsyncLocal, see https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/HttpContextAccessor.cs - IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); - services.AddSingleton(httpContextAccessor); - - var requestCache = new HttpContextRequestAppCache(httpContextAccessor); - var appCaches = AppCaches.Create(requestCache); - - services.ConfigureOptions(); - services.ConfigureOptions(); - - IProfiler profiler = GetWebProfiler(config); - - services.AddLogger(webHostEnvironment, config); - - ILoggerFactory loggerFactory = new SerilogLoggerFactory(); - - TypeLoader typeLoader = services.AddTypeLoader(Assembly.GetEntryAssembly(), loggerFactory, config); - - IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config); - return new UmbracoBuilder(services, config, typeLoader, loggerFactory, profiler, appCaches, tempHostingEnvironment); + throw new ArgumentNullException(nameof(builder)); } + // Add ASP.NET specific services + builder.Services.AddUnique(); + builder.Services.AddUnique(sp => + ActivatorUtilities.CreateInstance( + sp, + sp.GetRequiredService())); - /// - /// Adds core Umbraco services - /// - /// - /// This will not add any composers/components - /// - public static IUmbracoBuilder AddUmbracoCore(this IUmbracoBuilder builder) + builder.Services.AddHostedService(factory => factory.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + + // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now + builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( + DbProviderFactories.GetFactory, + factory.GetServices(), + factory.GetServices(), + factory.GetServices(), + factory.GetServices(), + factory.GetServices())); + + builder.AddCoreInitialServices(); + builder.AddTelemetryProviders(); + + // aspnet app lifetime mgmt + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + + return builder; + } + + /// + /// Add Umbraco hosted services + /// + public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder) + { + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(provider => + new ReportSiteTask( + provider.GetRequiredService>(), + provider.GetRequiredService())); + return builder; + } + + /// + /// Adds the Umbraco request profiler + /// + public static IUmbracoBuilder AddUmbracoProfiler(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + + builder.Services.AddMiniProfiler(options => { - if (builder is null) + // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile + options.ShouldProfile = request => false; + + // this is a default path and by default it performs a 'contains' check which will match our content controller + // (and probably other requests) and ignore them. + options.IgnoredPaths.Remove("/content/"); + }); + + builder.AddNotificationHandler(); + return builder; + } + + private static IUmbracoBuilder AddHttpClients(this IUmbracoBuilder builder) + { + builder.Services.AddHttpClient(); + builder.Services.AddHttpClient(Constants.HttpClients.IgnoreCertificateErrors) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { - throw new ArgumentNullException(nameof(builder)); - } - - // Add ASP.NET specific services - builder.Services.AddUnique(); - builder.Services.AddUnique(sp => ActivatorUtilities.CreateInstance(sp, sp.GetRequiredService())); - - builder.Services.AddHostedService(factory => factory.GetRequiredService()); - - builder.Services.AddSingleton(); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now - builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( - DbProviderFactories.GetFactory, - factory.GetServices(), - factory.GetServices(), - factory.GetServices(), - factory.GetServices(), - factory.GetServices() - )); - - builder.AddCoreInitialServices(); - builder.AddTelemetryProviders(); - - // aspnet app lifetime mgmt - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - - return builder; - } - - /// - /// Add Umbraco hosted services - /// - public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder) - { - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(provider => - new ReportSiteTask( - provider.GetRequiredService>(), - provider.GetRequiredService())); - return builder; - } - - private static IUmbracoBuilder AddHttpClients(this IUmbracoBuilder builder) - { - builder.Services.AddHttpClient(); - builder.Services.AddHttpClient(Constants.HttpClients.IgnoreCertificateErrors) - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }); - return builder; - } - - /// - /// Adds the Umbraco request profiler - /// - public static IUmbracoBuilder AddUmbracoProfiler(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - - builder.Services.AddMiniProfiler(options => - { - // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile - options.ShouldProfile = request => false; - - // this is a default path and by default it performs a 'contains' check which will match our content controller - // (and probably other requests) and ignore them. - options.IgnoredPaths.Remove("/content/"); + ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, }); + return builder; + } - builder.AddNotificationHandler(); - return builder; + public static IUmbracoBuilder AddMvcAndRazor(this IUmbracoBuilder builder, Action? mvcBuilding = null) + { + // TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important + // this will directly affect developers who need to call that themselves. + // We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. + // But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. + IMvcBuilder mvcBuilder = builder.Services + .AddControllersWithViews(); + + FixForDotnet6Preview1(builder.Services); + mvcBuilder.AddRazorRuntimeCompilation(); + + mvcBuilding?.Invoke(mvcBuilder); + + return builder; + } + + /// + /// Add runtime minifier support for Umbraco + /// + public static IUmbracoBuilder AddRuntimeMinifier(this IUmbracoBuilder builder) + { + // Add custom ISmidgeFileProvider to include the additional App_Plugins location + // to load assets from. + builder.Services.AddSingleton(f => + { + IWebHostEnvironment hostEnv = f.GetRequiredService(); + + return new SmidgeFileProvider( + hostEnv.WebRootFileProvider, + new GlobPatternFilterFileProvider( + hostEnv.ContentRootFileProvider, + // only include js or css files within App_Plugins + new[] { "/App_Plugins/**/*.js", "/App_Plugins/**/*.css" })); + }); + + builder.Services.AddUnique(); + builder.Services.AddSmidge(builder.Config.GetSection(Constants.Configuration.ConfigRuntimeMinification)); + + // Replace the Smidge request helper, in order to discourage the use of brotli since it's super slow + builder.Services.AddUnique(); + builder.Services.AddSmidgeNuglify(); + builder.Services.AddSmidgeInMemory(false); // it will be enabled based on config/cachebuster + + builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.ConfigureOptions(); + + return builder; + } + + /// + /// Adds all web based services required for Umbraco to run + /// + public static IUmbracoBuilder AddWebComponents(this IUmbracoBuilder builder) + { + // Add service session + // This can be overwritten by the user by adding their own call to AddSession + // since the last call of AddSession take precedence + builder.Services.AddSession(options => + { + options.Cookie.Name = "UMB_SESSION"; + options.Cookie.HttpOnly = true; + }); + + builder.Services.ConfigureOptions(); + builder.Services.ConfigureOptions(); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Transient()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Transient()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Transient()); + builder.AddUmbracoImageSharp(); + + // AspNetCore specific services + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + + // Password hasher + builder.Services.AddUnique(); + + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + + builder.Services.AddMultipleUnique(); + + builder.Services.AddUnique(); + + builder.Services.AddUnique(); + + builder.Services.AddUnique(serviceProvider => + { + return new MacroRenderer( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + }); + + builder.Services.AddSingleton(); + + // register the umbraco context factory + builder.Services.AddUnique(); + builder.Services.AddUnique(); + + var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList(); + builder.WithCollectionBuilder()? + .Add(umbracoApiControllerTypes); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + builder.Services.AddUnique(); + builder.Services.AddUnique(); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.AddHttpClients(); + + return builder; + } + + // TODO: Does this need to exist and/or be public? + public static IUmbracoBuilder AddWebServer(this IUmbracoBuilder builder) + { + // TODO: We need to figure out why this is needed and fix those endpoints to not need them, we don't want to change global things + // If using Kestrel: https://stackoverflow.com/a/55196057 + builder.Services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + builder.Services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + + return builder; + } + + private static IProfiler GetWebProfiler(IConfiguration config) + { + var isDebug = config.GetValue($"{Constants.Configuration.ConfigHosting}:Debug"); + + // create and start asap to profile boot + if (!isDebug) + { + // should let it be null, that's how MiniProfiler is meant to work, + // but our own IProfiler expects an instance so let's get one + return new NoopProfiler(); } - public static IUmbracoBuilder AddMvcAndRazor(this IUmbracoBuilder builder, Action? mvcBuilding = null) + var webProfiler = new WebProfiler(); + webProfiler.StartBoot(); + + return webProfiler; + } + + /// + /// HACK: returns an AspNetCoreHostingEnvironment that doesn't monitor changes to configuration.
+ /// We require this to create a TypeLoader during ConfigureServices.
+ /// Instances returned from this method shouldn't be registered in the service collection. + ///
+ private static IHostingEnvironment GetTemporaryHostingEnvironment( + IWebHostEnvironment webHostEnvironment, + IConfiguration config) + { + HostingSettings hostingSettings = + config.GetSection(Constants.Configuration.ConfigHosting).Get() ?? new HostingSettings(); + var wrappedHostingSettings = new OptionsMonitorAdapter(hostingSettings); + + WebRoutingSettings webRoutingSettings = + config.GetSection(Constants.Configuration.ConfigWebRouting).Get() ?? + new WebRoutingSettings(); + var wrappedWebRoutingSettings = new OptionsMonitorAdapter(webRoutingSettings); + + return new AspNetCoreHostingEnvironment( + wrappedHostingSettings, + wrappedWebRoutingSettings, + webHostEnvironment); + } + + /// + /// This fixes an issue for .NET6 Preview1, that in AddRazorRuntimeCompilation cannot remove the existing + /// IViewCompilerProvider. + /// + /// + /// When running .NET6 Preview1 there is an issue with looks to be fixed when running ASP.NET Core 6. + /// This issue is because the default implementation of IViewCompilerProvider has changed, so the + /// AddRazorRuntimeCompilation extension can't remove the default and replace with the runtimeviewcompiler. + /// This method basically does the same as the ASP.NET Core 6 version of AddRazorRuntimeCompilation + /// https://github.com/dotnet/aspnetcore/blob/f7dc5e24af7f9692a1db66741954b90b42d84c3a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L71-L80 + /// While running .NET5 this does nothing as the ImplementationType has another FullName, and this is handled by the + /// .NET5 version of AddRazorRuntimeCompilation + /// + private static void FixForDotnet6Preview1(IServiceCollection services) + { + ServiceDescriptor? compilerProvider = services.FirstOrDefault(f => + f.ServiceType == typeof(IViewCompilerProvider) && + f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && + f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler"); + + if (compilerProvider != null) { - // TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important - // this will directly affect developers who need to call that themselves. - // We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. - // But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. - IMvcBuilder mvcBuilder = builder.Services - .AddControllersWithViews(); - - FixForDotnet6Preview1(builder.Services); - mvcBuilder.AddRazorRuntimeCompilation(); - - mvcBuilding?.Invoke(mvcBuilder); - - return builder; - } - - /// - /// This fixes an issue for .NET6 Preview1, that in AddRazorRuntimeCompilation cannot remove the existing IViewCompilerProvider. - /// - /// - /// When running .NET6 Preview1 there is an issue with looks to be fixed when running ASP.NET Core 6. - /// This issue is because the default implementation of IViewCompilerProvider has changed, so the - /// AddRazorRuntimeCompilation extension can't remove the default and replace with the runtimeviewcompiler. - /// - /// This method basically does the same as the ASP.NET Core 6 version of AddRazorRuntimeCompilation - /// https://github.com/dotnet/aspnetcore/blob/f7dc5e24af7f9692a1db66741954b90b42d84c3a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L71-L80 - /// - /// While running .NET5 this does nothing as the ImplementationType has another FullName, and this is handled by the .NET5 version of AddRazorRuntimeCompilation - /// - private static void FixForDotnet6Preview1(IServiceCollection services) - { - var compilerProvider = services.FirstOrDefault(f => - f.ServiceType == typeof(IViewCompilerProvider) && - f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && - f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler"); - - if (compilerProvider != null) - { - services.Remove(compilerProvider); - } - } - - /// - /// Add runtime minifier support for Umbraco - /// - public static IUmbracoBuilder AddRuntimeMinifier(this IUmbracoBuilder builder) - { - // Add custom ISmidgeFileProvider to include the additional App_Plugins location - // to load assets from. - builder.Services.AddSingleton(f => - { - IWebHostEnvironment hostEnv = f.GetRequiredService(); - - return new SmidgeFileProvider( - hostEnv.WebRootFileProvider, - new GlobPatternFilterFileProvider( - hostEnv.ContentRootFileProvider, - // only include js or css files within App_Plugins - new[] { "/App_Plugins/**/*.js", "/App_Plugins/**/*.css" })); - }); - - builder.Services.AddUnique(); - builder.Services.AddSmidge(builder.Config.GetSection(Constants.Configuration.ConfigRuntimeMinification)); - // Replace the Smidge request helper, in order to discourage the use of brotli since it's super slow - builder.Services.AddUnique(); - builder.Services.AddSmidgeNuglify(); - builder.Services.AddSmidgeInMemory(false); // it will be enabled based on config/cachebuster - - builder.Services.AddUnique(); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.ConfigureOptions(); - - return builder; - } - - /// - /// Adds all web based services required for Umbraco to run - /// - public static IUmbracoBuilder AddWebComponents(this IUmbracoBuilder builder) - { - // Add service session - // This can be overwritten by the user by adding their own call to AddSession - // since the last call of AddSession take precedence - builder.Services.AddSession(options => - { - options.Cookie.Name = "UMB_SESSION"; - options.Cookie.HttpOnly = true; - }); - - builder.Services.ConfigureOptions(); - builder.Services.ConfigureOptions(); - builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - builder.AddUmbracoImageSharp(); - - // AspNetCore specific services - builder.Services.AddUnique(); - builder.AddNotificationHandler(); - - // Password hasher - builder.Services.AddUnique(); - - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddUnique(); - - builder.Services.AddMultipleUnique(); - - builder.Services.AddUnique(); - - builder.Services.AddUnique(); - - builder.Services.AddUnique(); - builder.Services.AddSingleton(); - - // register the umbraco context factory - - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList(); - builder.WithCollectionBuilder()? - .Add(umbracoApiControllerTypes); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - builder.AddHttpClients(); - - return builder; - } - - - // TODO: Does this need to exist and/or be public? - public static IUmbracoBuilder AddWebServer(this IUmbracoBuilder builder) - { - // TODO: We need to figure out why this is needed and fix those endpoints to not need them, we don't want to change global things - // If using Kestrel: https://stackoverflow.com/a/55196057 - builder.Services.Configure(options => - { - options.AllowSynchronousIO = true; - }); - builder.Services.Configure(options => - { - options.AllowSynchronousIO = true; - }); - - return builder; - } - - private static IProfiler GetWebProfiler(IConfiguration config) - { - var isDebug = config.GetValue($"{Cms.Core.Constants.Configuration.ConfigHosting}:Debug"); - // create and start asap to profile boot - if (!isDebug) - { - // should let it be null, that's how MiniProfiler is meant to work, - // but our own IProfiler expects an instance so let's get one - return new NoopProfiler(); - } - - var webProfiler = new WebProfiler(); - webProfiler.StartBoot(); - - return webProfiler; - } - - /// - /// HACK: returns an AspNetCoreHostingEnvironment that doesn't monitor changes to configuration.
- /// We require this to create a TypeLoader during ConfigureServices.
- /// Instances returned from this method shouldn't be registered in the service collection. - ///
- private static IHostingEnvironment GetTemporaryHostingEnvironment(IWebHostEnvironment webHostEnvironment, IConfiguration config) - { - var hostingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigHosting).Get() ?? new HostingSettings(); - var wrappedHostingSettings = new OptionsMonitorAdapter(hostingSettings); - - var webRoutingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigWebRouting).Get() ?? new WebRoutingSettings(); - var wrappedWebRoutingSettings = new OptionsMonitorAdapter(webRoutingSettings); - - return new AspNetCoreHostingEnvironment( - wrappedHostingSettings, - wrappedWebRoutingSettings, - webHostEnvironment); + services.Remove(compilerProvider); } } } diff --git a/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs index 6b0b87c7b7..a7f6190844 100644 --- a/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs +++ b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs @@ -1,17 +1,16 @@ -using System; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Cms.Web.Common.Events -{ - public class ActionExecutedEventArgs : EventArgs - { - public Controller Controller { get; set; } - public object Model { get; set; } +namespace Umbraco.Cms.Web.Common.Events; - public ActionExecutedEventArgs(Controller controller, object model) - { - Controller = controller; - Model = model; - } +public class ActionExecutedEventArgs : EventArgs +{ + public ActionExecutedEventArgs(Controller controller, object model) + { + Controller = controller; + Model = model; } + + public Controller Controller { get; set; } + + public object Model { get; set; } } diff --git a/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs index a98ab32f8b..94299142e0 100644 --- a/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs +++ b/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs @@ -1,44 +1,55 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Web.Common.Exceptions +namespace Umbraco.Cms.Web.Common.Exceptions; + +/// +/// Exception that occurs when an Umbraco form route string is invalid +/// +[Serializable] +public sealed class HttpUmbracoFormRouteStringException : Exception { /// - /// Exception that occurs when an Umbraco form route string is invalid + /// Initializes a new instance of the class. /// - [Serializable] - public sealed class HttpUmbracoFormRouteStringException : Exception + public HttpUmbracoFormRouteStringException() { - /// - /// Initializes a new instance of the class. - /// - public HttpUmbracoFormRouteStringException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that holds the contextual information about the source or destination. - private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message displayed to the client when the exception is thrown. - public HttpUmbracoFormRouteStringException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + /// + /// The , if any, that threw the current + /// exception. + /// + public HttpUmbracoFormRouteStringException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message displayed to the client when the exception is thrown. - /// The , if any, that threw the current exception. - public HttpUmbracoFormRouteStringException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that holds the contextual + /// information about the source or destination. + /// + private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs b/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs index 21bfd6f9ba..e0a42597ef 100644 --- a/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs @@ -1,20 +1,20 @@ -using System.Net; +using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; -namespace Umbraco.Extensions -{ - public static class ActionResultExtensions - { - public static bool IsSuccessStatusCode(this ActionResult actionResult) - { - var statusCode = actionResult switch - { - IStatusCodeActionResult x => x.StatusCode, - _ => (int?)null - }; +namespace Umbraco.Extensions; - return statusCode.HasValue && statusCode.Value >= (int)HttpStatusCode.OK && statusCode.Value < (int) HttpStatusCode.Ambiguous; - } +public static class ActionResultExtensions +{ + public static bool IsSuccessStatusCode(this ActionResult actionResult) + { + var statusCode = actionResult switch + { + IStatusCodeActionResult x => x.StatusCode, + _ => null, + }; + + return statusCode.HasValue && statusCode.Value >= (int)HttpStatusCode.OK && + statusCode.Value < (int)HttpStatusCode.Ambiguous; } } diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index fcd4dcb341..6d9481e666 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,136 +1,140 @@ -using System; -using System.IO; using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Serilog.Context; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// extensions for Umbraco +/// +public static class ApplicationBuilderExtensions { /// - /// extensions for Umbraco + /// Configures and use services required for using Umbraco /// - public static class ApplicationBuilderExtensions + public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app) + => new UmbracoApplicationBuilder(app); + + /// + /// Returns true if Umbraco is greater than + /// + public static bool UmbracoCanBoot(this IApplicationBuilder app) + => app.ApplicationServices.GetRequiredService().UmbracoCanBoot(); + + /// + /// Enables core Umbraco functionality + /// + public static IApplicationBuilder UseUmbracoCore(this IApplicationBuilder app) { - /// - /// Configures and use services required for using Umbraco - /// - public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app) - => new UmbracoApplicationBuilder(app); - - /// - /// Returns true if Umbraco is greater than - /// - public static bool UmbracoCanBoot(this IApplicationBuilder app) - => app.ApplicationServices.GetRequiredService().UmbracoCanBoot(); - - /// - /// Enables core Umbraco functionality - /// - public static IApplicationBuilder UseUmbracoCore(this IApplicationBuilder app) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - // Register our global threadabort enricher for logging - ThreadAbortExceptionEnricher threadAbortEnricher = app.ApplicationServices.GetRequiredService(); - LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context + throw new ArgumentNullException(nameof(app)); + } + if (!app.UmbracoCanBoot()) + { return app; } - /// - /// Enables middlewares required to run Umbraco - /// - /// - /// Must occur before UseRouting - /// - public static IApplicationBuilder UseUmbracoRouting(this IApplicationBuilder app) + // Register our global threadabort enricher for logging + ThreadAbortExceptionEnricher threadAbortEnricher = + app.ApplicationServices.GetRequiredService(); + LogContext.Push( + threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context + + return app; + } + + /// + /// Enables middlewares required to run Umbraco + /// + /// + /// Must occur before UseRouting + /// + public static IApplicationBuilder UseUmbracoRouting(this IApplicationBuilder app) + { + // TODO: This method could be internal or part of another call - this is a required system so should't be 'opt-in' + if (app == null) { - // TODO: This method could be internal or part of another call - this is a required system so should't be 'opt-in' - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) - { - app.UseStaticFiles(); // We need static files to show the nice error page. - app.UseMiddleware(); - } - else - { - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); - } + if (!app.UmbracoCanBoot()) + { + app.UseStaticFiles(); // We need static files to show the nice error page. + app.UseMiddleware(); + } + else + { + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + } + return app; + } + + /// + /// Adds request based serilog enrichers to the LogContext for each request + /// + public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!app.UmbracoCanBoot()) + { return app; } - /// - /// Adds request based serilog enrichers to the LogContext for each request - /// - public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app) + app.UseMiddleware(); + + return app; + } + + /// + /// Allow static file access for App_Plugins folders + /// + /// + /// + public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) + { + IHostEnvironment hostingEnvironment = app.ApplicationServices.GetRequiredService(); + + var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); + if (Directory.Exists(pluginFolder)) { - if (app == null) + IOptionsMonitor umbracoPluginSettings = + app.ApplicationServices.GetRequiredService>(); + + var pluginFileProvider = new UmbracoPluginPhysicalFileProvider( + pluginFolder, + umbracoPluginSettings); + + IWebHostEnvironment? webHostEnvironment = app.ApplicationServices.GetService(); + + if (webHostEnvironment is not null) { - throw new ArgumentNullException(nameof(app)); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite( + new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider)); } - - if (!app.UmbracoCanBoot()) - return app; - - app.UseMiddleware(); - - return app; } - /// - /// Allow static file access for App_Plugins folders - /// - /// - /// - public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) - { - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - - var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); - if (Directory.Exists(pluginFolder)) - { - var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); - - var pluginFileProvider = new UmbracoPluginPhysicalFileProvider( - pluginFolder, - umbracoPluginSettings); - - IWebHostEnvironment? webHostEnvironment = app.ApplicationServices.GetService(); - - if (webHostEnvironment is not null) - { - webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider)); - } - } - - return app; - } + return app; } } diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs index cb403e5e7e..a0b6a27dc9 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs @@ -1,35 +1,32 @@ -using System; using Microsoft.AspNetCore.DataProtection.Infrastructure; -using Umbraco.Extensions; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Contains extension methods for the interface. +/// +public static class ApplicationDiscriminatorExtensions { + private static string? applicationId; + /// - /// Contains extension methods for the interface. + /// Gets an application id which respects downstream customizations. /// - public static class ApplicationDiscriminatorExtensions + /// + /// Hashed to obscure any unintended infrastructure details e.g. the default value is ContentRootPath. + /// + public static string? GetApplicationId(this IApplicationDiscriminator applicationDiscriminator) { - private static string? s_applicationId; - - /// - /// Gets an application id which respects downstream customizations. - /// - /// - /// Hashed to obscure any unintended infrastructure details e.g. the default value is ContentRootPath. - /// - public static string? GetApplicationId(this IApplicationDiscriminator applicationDiscriminator) + if (applicationId != null) { - if (s_applicationId != null) - { - return s_applicationId; - } - - if (applicationDiscriminator == null) - { - throw new ArgumentNullException(nameof(applicationDiscriminator)); - } - - return s_applicationId = applicationDiscriminator.Discriminator?.GenerateHash(); + return applicationId; } + + if (applicationDiscriminator == null) + { + throw new ArgumentNullException(nameof(applicationDiscriminator)); + } + + return applicationId = applicationDiscriminator.Discriminator?.GenerateHash(); } } diff --git a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs index c51a376145..17b620ab51 100644 --- a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs @@ -1,37 +1,52 @@ -using System; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class BlockListTemplateExtensions { - public static class BlockListTemplateExtensions + public const string DefaultFolder = "blocklist/"; + public const string DefaultTemplate = "default"; + + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, BlockListModel? model, string template = DefaultTemplate) { - public const string DefaultFolder = "blocklist/"; - public const string DefaultTemplate = "default"; - - public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, BlockListModel? model, string template = DefaultTemplate) + if (model?.Count == 0) { - if (model?.Count == 0) return new HtmlString(string.Empty); - - var view = DefaultFolder + template; - return html.Partial(view, model); + return new HtmlString(string.Empty); } - public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + var view = DefaultFolder + template; + return html.Partial(view, model); + } - public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) + => GetBlockListHtml(html, property.GetValue() as BlockListModel, template); - public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) + => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + { + if (propertyAlias == null) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); - - return GetBlockListHtml(html, prop?.GetValue() as BlockListModel, template); + throw new ArgumentNullException(nameof(propertyAlias)); } + + if (string.IsNullOrWhiteSpace(propertyAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); + } + + IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); + if (prop == null) + { + throw new InvalidOperationException("No property type found with alias " + propertyAlias); + } + + return GetBlockListHtml(html, prop.GetValue() as BlockListModel, template); } } diff --git a/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs index e3c38c00ca..7f15bc844c 100644 --- a/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -6,53 +5,50 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for the cache helper +/// +public static class CacheHelperExtensions { /// - /// Extension methods for the cache helper + /// Outputs and caches a partial view in MVC /// - public static class CacheHelperExtensions + /// + /// + /// + /// + /// + /// + /// + /// used to cache the partial view, this key could change if it is cached by page or by member + /// + /// + public static IHtmlContent? CachedPartialView( + this AppCaches appCaches, + IHostingEnvironment hostingEnvironment, + IUmbracoContext umbracoContext, + IHtmlHelper htmlHelper, + string partialViewName, + object model, + TimeSpan cacheTimeout, + string cacheKey, + ViewDataDictionary? viewData = null) { - /// - /// Outputs and caches a partial view in MVC - /// - /// - /// - /// - /// - /// - /// - /// - /// used to cache the partial view, this key could change if it is cached by page or by member - /// - /// - public static IHtmlContent? CachedPartialView( - this AppCaches appCaches, - IHostingEnvironment hostingEnvironment, - IUmbracoContext umbracoContext, - IHtmlHelper htmlHelper, - string partialViewName, - object model, - TimeSpan cacheTimeout, - string cacheKey, - ViewDataDictionary? viewData = null - ) + // disable cached partials in debug mode: http://issues.umbraco.org/issue/U4-5940 + // disable cached partials in preview mode: https://github.com/umbraco/Umbraco-CMS/issues/10384 + if (hostingEnvironment.IsDebugMode || umbracoContext.InPreviewMode) { - //disable cached partials in debug mode: http://issues.umbraco.org/issue/U4-5940 - //disable cached partials in preview mode: https://github.com/umbraco/Umbraco-CMS/issues/10384 - if (hostingEnvironment.IsDebugMode || (umbracoContext?.InPreviewMode == true)) - { - // just return a normal partial view instead - return htmlHelper.Partial(partialViewName, model, viewData); - } - - var result = appCaches.RuntimeCache.GetCacheItem( - CoreCacheHelperExtensions.PartialViewCacheKey + cacheKey, - () => new HtmlString(htmlHelper.Partial(partialViewName, model, viewData).ToHtmlString()), - timeout: cacheTimeout); - - return result; + // just return a normal partial view instead + return htmlHelper.Partial(partialViewName, model, viewData); } + IHtmlContent? result = appCaches.RuntimeCache.GetCacheItem( + CoreCacheHelperExtensions.PartialViewCacheKey + cacheKey, + () => new HtmlString(htmlHelper.Partial(partialViewName, model, viewData).ToHtmlString()), + cacheTimeout); + + return result; } } diff --git a/src/Umbraco.Web.Common/Extensions/ControllerActionEndpointConventionBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerActionEndpointConventionBuilderExtensions.cs index 2436787296..6f3e559005 100644 --- a/src/Umbraco.Web.Common/Extensions/ControllerActionEndpointConventionBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ControllerActionEndpointConventionBuilderExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; @@ -8,45 +6,45 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ControllerActionEndpointConventionBuilderExtensions { - public static class ControllerActionEndpointConventionBuilderExtensions - { - /// - /// Allows for defining a callback to set the returned for the current request for this route - /// - public static void ForUmbracoPage( - this ControllerActionEndpointConventionBuilder builder, - Func findContent) - => builder.Add(convention => + /// + /// Allows for defining a callback to set the returned for the current request for + /// this route + /// + public static void ForUmbracoPage( + this ControllerActionEndpointConventionBuilder builder, + Func findContent) + => builder.Add(convention => + { + // filter out matched endpoints that are suppressed + if (convention.Metadata.OfType().FirstOrDefault()?.SuppressMatching != true) { - // filter out matched endpoints that are suppressed - if (convention.Metadata.OfType().FirstOrDefault()?.SuppressMatching != true) + // Get the controller action descriptor + ControllerActionDescriptor? actionDescriptor = + convention.Metadata.OfType().FirstOrDefault(); + if (actionDescriptor != null) { - // Get the controller action descriptor - ControllerActionDescriptor? actionDescriptor = convention.Metadata.OfType().FirstOrDefault(); - if (actionDescriptor != null) + // This is more or less like the IApplicationModelProvider, it allows us to add filters, etc... to the ControllerActionDescriptor + // dynamically. Here we will add our custom virtual page filter along with a callback in the endpoint's metadata + // to execute in order to find the IPublishedContent for the request. + var filter = new UmbracoVirtualPageFilterAttribute(); + + // Check if this already contains this filter since we don't want it applied twice. + // This could occur if the controller being routed is IVirtualPageController AND + // is being routed with ForUmbracoPage. In that case, ForUmbracoPage wins + // because the UmbracoVirtualPageFilterAttribute will check for the metadata first since + // that is more explicit and flexible in case the same controller is routed multiple times. + if (!actionDescriptor.FilterDescriptors.Any(x => x.Filter is UmbracoVirtualPageFilterAttribute)) { - // This is more or less like the IApplicationModelProvider, it allows us to add filters, etc... to the ControllerActionDescriptor - // dynamically. Here we will add our custom virtual page filter along with a callback in the endpoint's metadata - // to execute in order to find the IPublishedContent for the request. - - var filter = new UmbracoVirtualPageFilterAttribute(); - - // Check if this already contains this filter since we don't want it applied twice. - // This could occur if the controller being routed is IVirtualPageController AND - // is being routed with ForUmbracoPage. In that case, ForUmbracoPage wins - // because the UmbracoVirtualPageFilterAttribute will check for the metadata first since - // that is more explicit and flexible in case the same controller is routed multiple times. - if (!actionDescriptor.FilterDescriptors.Any(x => x.Filter is UmbracoVirtualPageFilterAttribute)) - { - actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(filter, 0)); - convention.Metadata.Add(filter); - } - - convention.Metadata.Add(new CustomRouteContentFinderDelegate(findContent)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(filter, 0)); + convention.Metadata.Add(filter); } + + convention.Metadata.Add(new CustomRouteContentFinderDelegate(findContent)); } - }); - } + } + }); } diff --git a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs index 911ecee8e5..b164ce04c1 100644 --- a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs @@ -1,55 +1,47 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ControllerExtensions { - public static class ControllerExtensions + /// + /// Runs the authentication process + /// + /// + /// + public static async Task AuthenticateBackOfficeAsync(this ControllerBase controller) => + await controller.HttpContext.AuthenticateBackOfficeAsync(); + + /// + /// Return the controller name from the controller type + /// + /// + /// + public static string GetControllerName(Type controllerType) { - /// - /// Runs the authentication process - /// - /// - /// - public static async Task AuthenticateBackOfficeAsync(this ControllerBase controller) + if (!controllerType.Name.EndsWith("Controller") && !controllerType.Name.EndsWith("Controller`1")) { - return await controller.HttpContext.AuthenticateBackOfficeAsync(); + throw new InvalidOperationException("The controller type " + controllerType + + " does not follow conventions, MVC Controller class names must be suffixed with the term 'Controller'"); } - /// - /// Return the controller name from the controller type - /// - /// - /// - public static string GetControllerName(Type controllerType) - { - if (!controllerType.Name.EndsWith("Controller") && !controllerType.Name.EndsWith("Controller`1")) - { - throw new InvalidOperationException("The controller type " + controllerType + " does not follow conventions, MVC Controller class names must be suffixed with the term 'Controller'"); - } - return controllerType.Name.Substring(0, controllerType.Name.LastIndexOf("Controller")); - } - - /// - /// Return the controller name from the controller instance - /// - /// - /// - public static string GetControllerName(this Controller controllerInstance) - { - return GetControllerName(controllerInstance.GetType()); - } - - /// - /// Return the controller name from the controller type - /// - /// - /// - /// - public static string GetControllerName() - { - return GetControllerName(typeof(T)); - } + return controllerType.Name[..controllerType.Name.LastIndexOf("Controller", StringComparison.Ordinal)]; } + + /// + /// Return the controller name from the controller instance + /// + /// + /// + public static string GetControllerName(this Controller controllerInstance) => + GetControllerName(controllerInstance.GetType()); + + /// + /// Return the controller name from the controller type + /// + /// + /// + /// + public static string GetControllerName() => GetControllerName(typeof(T)); } diff --git a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs index 7c0c382818..101eb0f3a1 100644 --- a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs @@ -1,148 +1,143 @@ -using System; using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class EndpointRouteBuilderExtensions { - public static class EndpointRouteBuilderExtensions + /// + /// Used to map Umbraco controllers consistently + /// + public static void MapUmbracoRoute( + this IEndpointRouteBuilder endpoints, + Type controllerType, + string rootSegment, + string? areaName, + string? prefixPathSegment, + string defaultAction = "Index", + bool includeControllerNameInRoute = true, + object? constraints = null) { - /// - /// Used to map Umbraco controllers consistently - /// - public static void MapUmbracoRoute( - this IEndpointRouteBuilder endpoints, - Type controllerType, - string rootSegment, - string? areaName, - string? prefixPathSegment, - string defaultAction = "Index", - bool includeControllerNameInRoute = true, - object? constraints = null) + var controllerName = ControllerExtensions.GetControllerName(controllerType); + + // build the route pattern + var pattern = new StringBuilder(rootSegment); + if (!prefixPathSegment.IsNullOrWhiteSpace()) { - var controllerName = ControllerExtensions.GetControllerName(controllerType); - - // build the route pattern - var pattern = new StringBuilder(rootSegment); - if (!prefixPathSegment.IsNullOrWhiteSpace()) - { - pattern.Append('/').Append(prefixPathSegment); - } - - if (includeControllerNameInRoute) - { - pattern.Append('/').Append(controllerName); - } - - pattern.Append("/{action}/{id?}"); - - var defaults = defaultAction.IsNullOrWhiteSpace() - ? (object)new { controller = controllerName } - : new { controller = controllerName, action = defaultAction }; - - if (areaName.IsNullOrWhiteSpace()) - { - endpoints.MapControllerRoute( - - // named consistently - $"umbraco-{areaName}-{controllerName}".ToLowerInvariant(), - pattern.ToString().ToLowerInvariant(), - defaults, - constraints); - } - else - { - endpoints.MapAreaControllerRoute( - - // named consistently - $"umbraco-{areaName}-{controllerName}".ToLowerInvariant(), - areaName!, - pattern.ToString().ToLowerInvariant(), - defaults, - constraints); - } + pattern.Append('/').Append(prefixPathSegment); } - /// - /// Used to map Umbraco controllers consistently - /// - /// The type to route - public static void MapUmbracoRoute( - this IEndpointRouteBuilder endpoints, - string rootSegment, - string areaName, - string? prefixPathSegment, - string defaultAction = "Index", - bool includeControllerNameInRoute = true, - object? constraints = null) - where T : ControllerBase - => endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints); - - /// - /// Used to map controllers as Umbraco API routes consistently - /// - /// The type to route - public static void MapUmbracoApiRoute( - this IEndpointRouteBuilder endpoints, - string rootSegment, - string areaName, - bool isBackOffice, - string defaultAction = "Index", - object? constraints = null) - where T : ControllerBase - => endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints); - - /// - /// Used to map controllers as Umbraco API routes consistently - /// - public static void MapUmbracoApiRoute( - this IEndpointRouteBuilder endpoints, - Type controllerType, - string rootSegment, - string? areaName, - bool isBackOffice, - string defaultAction = "Index", - object? constraints = null) + if (includeControllerNameInRoute) { - string? prefixPathSegment = isBackOffice - ? areaName.IsNullOrWhiteSpace() - ? $"{Cms.Core.Constants.Web.Mvc.BackOfficePathSegment}/Api" - : $"{Cms.Core.Constants.Web.Mvc.BackOfficePathSegment}/{areaName}" - : areaName.IsNullOrWhiteSpace() - ? "Api" - : areaName; - - endpoints.MapUmbracoRoute( - controllerType, - rootSegment, - areaName, - prefixPathSegment, - defaultAction, - true, - constraints); + pattern.Append('/').Append(controllerName); } - public static void MapUmbracoSurfaceRoute( - this IEndpointRouteBuilder endpoints, - Type controllerType, - string rootSegment, - string? areaName, - string defaultAction = "Index", - bool includeControllerNameInRoute = true, - object? constraints = null) - { - // If there is an area name it's a plugin controller, and we should use the area name instead of surface - string prefixPathSegment = areaName.IsNullOrWhiteSpace() ? "Surface" : areaName!; + pattern.Append("/{action}/{id?}"); - endpoints.MapUmbracoRoute( - controllerType, - rootSegment, - areaName, - prefixPathSegment, - defaultAction, - includeControllerNameInRoute, + var defaults = defaultAction.IsNullOrWhiteSpace() + ? (object)new { controller = controllerName } + : new { controller = controllerName, action = defaultAction }; + + if (areaName.IsNullOrWhiteSpace()) + { + endpoints.MapControllerRoute( + $"umbraco-{areaName}-{controllerName}".ToLowerInvariant(), + pattern.ToString().ToLowerInvariant(), + defaults, + constraints); + } + else + { + endpoints.MapAreaControllerRoute( + $"umbraco-{areaName}-{controllerName}".ToLowerInvariant(), + areaName!, + pattern.ToString().ToLowerInvariant(), + defaults, constraints); } } + + /// + /// Used to map Umbraco controllers consistently + /// + /// The type to route + public static void MapUmbracoRoute( + this IEndpointRouteBuilder endpoints, + string rootSegment, + string areaName, + string? prefixPathSegment, + string defaultAction = "Index", + bool includeControllerNameInRoute = true, + object? constraints = null) + where T : ControllerBase + => endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints); + + /// + /// Used to map controllers as Umbraco API routes consistently + /// + /// The type to route + public static void MapUmbracoApiRoute( + this IEndpointRouteBuilder endpoints, + string rootSegment, + string areaName, + bool isBackOffice, + string defaultAction = "Index", + object? constraints = null) + where T : ControllerBase + => endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints); + + /// + /// Used to map controllers as Umbraco API routes consistently + /// + public static void MapUmbracoApiRoute( + this IEndpointRouteBuilder endpoints, + Type controllerType, + string rootSegment, + string? areaName, + bool isBackOffice, + string defaultAction = "Index", + object? constraints = null) + { + var prefixPathSegment = isBackOffice + ? areaName.IsNullOrWhiteSpace() + ? $"{Constants.Web.Mvc.BackOfficePathSegment}/Api" + : $"{Constants.Web.Mvc.BackOfficePathSegment}/{areaName}" + : areaName.IsNullOrWhiteSpace() + ? "Api" + : areaName; + + endpoints.MapUmbracoRoute( + controllerType, + rootSegment, + areaName, + prefixPathSegment, + defaultAction, + true, + constraints); + } + + public static void MapUmbracoSurfaceRoute( + this IEndpointRouteBuilder endpoints, + Type controllerType, + string rootSegment, + string? areaName, + string defaultAction = "Index", + bool includeControllerNameInRoute = true, + object? constraints = null) + { + // If there is an area name it's a plugin controller, and we should use the area name instead of surface + var prefixPathSegment = areaName.IsNullOrWhiteSpace() ? "Surface" : areaName!; + + endpoints.MapUmbracoRoute( + controllerType, + rootSegment, + areaName, + prefixPathSegment, + defaultAction, + includeControllerNameInRoute, + constraints); + } } diff --git a/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs index 35a2882bdd..4ecf44cc21 100644 --- a/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs @@ -1,19 +1,17 @@ -using System.Linq; using Microsoft.Extensions.FileProviders; -namespace Umbraco.Extensions -{ - internal static class FileProviderExtensions - { - public static IFileProvider ConcatComposite(this IFileProvider fileProvider, params IFileProvider[] fileProviders) - { - var existingFileProviders = fileProvider switch - { - CompositeFileProvider compositeFileProvider => compositeFileProvider.FileProviders, - _ => new[] { fileProvider } - }; +namespace Umbraco.Extensions; - return new CompositeFileProvider(existingFileProviders.Concat(fileProviders)); - } +internal static class FileProviderExtensions +{ + public static IFileProvider ConcatComposite(this IFileProvider fileProvider, params IFileProvider[] fileProviders) + { + IEnumerable? existingFileProviders = fileProvider switch + { + CompositeFileProvider compositeFileProvider => compositeFileProvider.FileProviders, + _ => new[] { fileProvider }, + }; + + return new CompositeFileProvider(existingFileProviders.Concat(fileProviders)); } } diff --git a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs index a1d6ab4977..c91d0fb6c2 100644 --- a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs @@ -1,104 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FormCollectionExtensions { - public static class FormCollectionExtensions + /// + /// Converts a dictionary object to a query string representation such as: + /// firstname=shannon&lastname=deminick + /// + /// + /// Any keys found in this collection will be removed from the output + /// + public static string ToQueryString(this FormCollection? items, params string[] keysToIgnore) { - /// - /// Converts a dictionary object to a query string representation such as: - /// firstname=shannon&lastname=deminick - /// - /// - /// Any keys found in this collection will be removed from the output - /// - public static string ToQueryString(this FormCollection items, params string[] keysToIgnore) + if (items == null) { - if (items == null) return ""; - if (items.Any() == false) return ""; - - var builder = new StringBuilder(); - foreach (var (key, value) in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) - builder.Append($"{key}={value}&"); - return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); + return string.Empty; } - /// - /// Converts the FormCollection to a dictionary - /// - /// - /// - public static IDictionary ToDictionary(this FormCollection items) + if (items.Any() == false) { - return items.ToDictionary(x => x.Key, x => (object)x.Value); + return string.Empty; } - /// - /// Returns the value of a mandatory item in the FormCollection - /// - /// - /// - /// - public static string GetRequiredString(this FormCollection items, string key) + var builder = new StringBuilder(); + foreach ((var key, StringValues value) in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) { - if (items.HasKey(key) == false) - throw new ArgumentNullException("The " + key + " query string parameter was not found but is required"); - return items.Single(x => x.Key.InvariantEquals(key)).Value; + builder.Append($"{key}={value}&"); } - /// - /// Checks if the collection contains the key - /// - /// - /// - /// - public static bool HasKey(this FormCollection items, string key) + return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); + } + + /// + /// Converts the FormCollection to a dictionary + /// + /// + /// + public static IDictionary ToDictionary(this FormCollection items) => + items.ToDictionary(x => x.Key, x => (object)x.Value); + + /// + /// Returns the value of a mandatory item in the FormCollection + /// + /// + /// + /// + public static string GetRequiredString(this FormCollection items, string key) + { + if (items.HasKey(key) == false) { - return items.Any(x => x.Key.InvariantEquals(key)); + throw new ArgumentNullException("The " + key + " query string parameter was not found but is required"); } - /// - /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert a null object is returned. - /// - /// - /// - /// - /// - public static T? GetValue(this FormCollection items, string key) - { - if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) - { - return default; - } + return items.Single(x => x.Key.InvariantEquals(key)).Value; + } - var converted = val.TryConvertTo(); - return converted.Success - ? converted.Result - : default; + /// + /// Checks if the collection contains the key + /// + /// + /// + /// + public static bool HasKey(this FormCollection items, string key) => items.Any(x => x.Key.InvariantEquals(key)); + + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't + /// convert a null object is returned. + /// + /// + /// + /// + /// + public static T? GetValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out StringValues val) == false || string.IsNullOrEmpty(val)) + { + return default; } - /// - /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert or the query string is no there an exception is thrown - /// - /// - /// - /// - /// - public static T GetRequiredValue(this FormCollection items, string key) - { - if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) - { - throw new InvalidOperationException($"The required query string parameter {key} is missing"); - } + Attempt converted = val.TryConvertTo(); + return converted.Success + ? converted.Result + : default; + } - var converted = val.TryConvertTo(); - return converted.Success - ? converted.Result! - : throw new InvalidOperationException($"The required query string parameter {key} cannot be converted to type {typeof(T)}"); + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't + /// convert or the query string is no there an exception is thrown + /// + /// + /// + /// + /// + public static T GetRequiredValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out StringValues val) == false || string.IsNullOrEmpty(val)) + { + throw new InvalidOperationException($"The required query string parameter {key} is missing"); } + + Attempt converted = val.TryConvertTo(); + return converted.Success + ? converted.Result! + : throw new InvalidOperationException( + $"The required query string parameter {key} cannot be converted to type {typeof(T)}"); } } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs index dabefbc3ad..bdacae95ef 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs @@ -1,5 +1,5 @@ -using System; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -7,300 +7,352 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FriendlyImageCropperTemplateExtensions { - public static class FriendlyImageCropperTemplateExtensions - { - private static IImageUrlGenerator ImageUrlGenerator { get; } = StaticServiceProvider.Instance.GetRequiredService(); + private static IImageUrlGenerator ImageUrlGenerator { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - private static IPublishedValueFallback PublishedValueFallback { get; } = StaticServiceProvider.Instance.GetRequiredService(); + private static IPublishedValueFallback PublishedValueFallback { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - private static IPublishedUrlProvider PublishedUrlProvider { get; } = StaticServiceProvider.Instance.GetRequiredService(); + private static IPublishedUrlProvider PublishedUrlProvider { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - /// - /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The crop alias e.g. thumbnail. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - string cropAlias, - UrlMode urlMode = UrlMode.Default) => - mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); + /// + /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias) on the + /// IPublishedContent item. + /// + /// The IPublishedContent item. + /// The crop alias e.g. thumbnail. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + string cropAlias, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - /// - /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias in the MediaWithCrops content item) on the MediaWithCrops item. - /// - /// The MediaWithCrops item. - /// The crop alias e.g. thumbnail. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias, UrlMode urlMode = UrlMode.Default) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); + /// + /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias in the + /// MediaWithCrops content item) on the MediaWithCrops item. + /// + /// The MediaWithCrops item. + /// The crop alias e.g. thumbnail. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias, UrlMode urlMode = UrlMode.Default) + => mediaWithCrops.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - /// - /// Gets the crop URL by using only the specified . - /// - /// The media item. - /// The image cropper value. - /// The crop alias. - /// The url mode. - /// - /// The image crop URL. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - ImageCropperValue imageCropperValue, - string cropAlias, - UrlMode urlMode = UrlMode.Default) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// The url mode. + /// + /// The image crop URL. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + ImageCropperValue imageCropperValue, + string cropAlias, + UrlMode urlMode = UrlMode.Default) + => mediaItem.GetCropUrl(imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - /// - /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The property alias of the property containing the JSON data e.g. umbracoFile. - /// The crop alias e.g. thumbnail. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - string propertyAlias, - string cropAlias, - UrlMode urlMode = UrlMode.Default) => - mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); + /// + /// Gets the underlying image processing service URL by the crop alias using the specified property containing the + /// image cropper JSON data on the IPublishedContent item. + /// + /// The IPublishedContent item. + /// The property alias of the property containing the JSON data e.g. umbracoFile. + /// The crop alias e.g. thumbnail. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + string propertyAlias, + string cropAlias, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - /// - /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the MediaWithCrops content item. - /// - /// The MediaWithCrops item. - /// The property alias of the property containing the JSON data e.g. umbracoFile. - /// The crop alias e.g. thumbnail. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias, UrlMode urlMode = UrlMode.Default) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); + /// + /// Gets the underlying image processing service URL by the crop alias using the specified property containing the + /// image cropper JSON data on the MediaWithCrops content item. + /// + /// The MediaWithCrops item. + /// The property alias of the property containing the JSON data e.g. umbracoFile. + /// The crop alias e.g. thumbnail. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias, UrlMode urlMode = UrlMode.Default) + => mediaWithCrops.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - /// - /// Gets the underlying image processing service URL from the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The width of the output image. - /// The height of the output image. - /// Property alias of the property containing the JSON data. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point, to generate an output image using the focal point instead of the predefined crop. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - int? width = null, - int? height = null, - string propertyAlias = Cms.Core.Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - UrlMode urlMode = UrlMode.Default) - => mediaItem.GetCropUrl( - ImageUrlGenerator, - PublishedValueFallback, - PublishedUrlProvider, - width, - height, - propertyAlias, - cropAlias, - quality, - imageCropMode, - imageCropAnchor, - preferFocalPoint, - useCropDimensions, - cacheBuster, - furtherOptions, - urlMode - ); + /// + /// Gets the underlying image processing service URL from the IPublishedContent item. + /// + /// The IPublishedContent item. + /// The width of the output image. + /// The height of the output image. + /// Property alias of the property containing the JSON data. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point, to generate an output image using the focal point instead of the + /// predefined crop. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + UrlMode urlMode = UrlMode.Default) + => mediaItem.GetCropUrl( + ImageUrlGenerator, + PublishedValueFallback, + PublishedUrlProvider, + width, + height, + propertyAlias, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBuster, + furtherOptions, + urlMode); - /// - /// Gets the underlying image processing service URL from the MediaWithCrops item. - /// - /// The MediaWithCrops item. - /// The width of the output image. - /// The height of the output image. - /// Property alias of the property containing the JSON data. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point, to generate an output image using the focal point instead of the predefined crop. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this MediaWithCrops mediaWithCrops, - int? width = null, - int? height = null, - string propertyAlias = Cms.Core.Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - UrlMode urlMode = UrlMode.Default) - => mediaWithCrops.GetCropUrl( - ImageUrlGenerator, - PublishedValueFallback, - PublishedUrlProvider, - width, - height, - propertyAlias, - cropAlias, - quality, - imageCropMode, - imageCropAnchor, - preferFocalPoint, - useCropDimensions, - cacheBuster, - furtherOptions, - urlMode - ); + /// + /// Gets the underlying image processing service URL from the MediaWithCrops item. + /// + /// The MediaWithCrops item. + /// The width of the output image. + /// The height of the output image. + /// Property alias of the property containing the JSON data. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point, to generate an output image using the focal point instead of the + /// predefined crop. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this MediaWithCrops mediaWithCrops, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + UrlMode urlMode = UrlMode.Default) + => mediaWithCrops.GetCropUrl( + ImageUrlGenerator, + PublishedValueFallback, + PublishedUrlProvider, + width, + height, + propertyAlias, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBuster, + furtherOptions, + urlMode); - /// - /// Gets the underlying image processing service URL from the image path. - /// - /// The image URL. - /// The width of the output image. - /// The height of the output image. - /// The JSON data from the Umbraco Core Image Cropper property editor. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this string imageUrl, - int? width = null, - int? height = null, - string? imageCropperValue = null, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - string? cacheBusterValue = null, - string? furtherOptions = null) - => imageUrl.GetCropUrl( - ImageUrlGenerator, - width, - height, - imageCropperValue, - cropAlias, - quality, - imageCropMode, - imageCropAnchor, - preferFocalPoint, - useCropDimensions, - cacheBusterValue, - furtherOptions - ); + /// + /// Gets the underlying image processing service URL from the image path. + /// + /// The image URL. + /// The width of the output image. + /// The height of the output image. + /// The JSON data from the Umbraco Core Image Cropper property editor. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point to generate an output image using the focal point instead of the + /// predefined crop if there is one. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this string imageUrl, + int? width = null, + int? height = null, + string? imageCropperValue = null, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string? cacheBusterValue = null, + string? furtherOptions = null) + => imageUrl.GetCropUrl( + ImageUrlGenerator, + width, + height, + imageCropperValue, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBusterValue, + furtherOptions); - /// - /// Gets the underlying image processing service URL from the image path. - /// - /// The image URL. - /// The crop data set. - /// The width of the output image. - /// The height of the output image. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this string imageUrl, - ImageCropperValue cropDataSet, - int? width = null, - int? height = null, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - string? cacheBusterValue = null, - string? furtherOptions = null) - => imageUrl.GetCropUrl( - ImageUrlGenerator, - cropDataSet, - width, - height, - cropAlias, - quality, imageCropMode, - imageCropAnchor, - preferFocalPoint, - useCropDimensions, - cacheBusterValue, - furtherOptions - ); + /// + /// Gets the underlying image processing service URL from the image path. + /// + /// The image URL. + /// The crop data set. + /// The width of the output image. + /// The height of the output image. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point to generate an output image using the focal point instead of the + /// predefined crop if there is one. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this string imageUrl, + ImageCropperValue cropDataSet, + int? width = null, + int? height = null, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string? cacheBusterValue = null, + string? furtherOptions = null) + => imageUrl.GetCropUrl( + ImageUrlGenerator, + cropDataSet, + width, + height, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBusterValue, + furtherOptions); - - [Obsolete("Use GetCropUrl to merge local and media crops, get automatic cache buster value and have more parameters.")] - public static string GetLocalCropUrl( - this MediaWithCrops mediaWithCrops, - string alias, - string? cacheBusterValue = null) => mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, ImageUrlGenerator, cacheBusterValue: cacheBusterValue); - } + [Obsolete( + "Use GetCropUrl to merge local and media crops, get automatic cache buster value and have more parameters.")] + public static string GetLocalCropUrl( + this MediaWithCrops mediaWithCrops, + string alias, + string? cacheBusterValue = null) => mediaWithCrops.LocalCrops.Src + + mediaWithCrops.LocalCrops.GetCropUrl(alias, ImageUrlGenerator, cacheBusterValue: cacheBusterValue); } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index ed53832cc5..4e22615ddf 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Data; using Examine; using Microsoft.Extensions.DependencyInjection; @@ -13,538 +11,595 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FriendlyPublishedContentExtensions { - public static class FriendlyPublishedContentExtensions + private static IVariationContextAccessor VariationContextAccessor { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedModelFactory PublishedModelFactory { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedUrlProvider PublishedUrlProvider { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IUserService UserService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IUmbracoContextAccessor UmbracoContextAccessor { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static ISiteDomainMapper SiteDomainHelper { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IExamineManager ExamineManager { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IFileService FileService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IOptions WebRoutingSettings { get; } = + StaticServiceProvider.Instance.GetRequiredService>(); + + private static IContentTypeService ContentTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedValueFallback PublishedValueFallback { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedSnapshot? PublishedSnapshot { - private static IVariationContextAccessor VariationContextAccessor { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IPublishedModelFactory PublishedModelFactory { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IPublishedUrlProvider PublishedUrlProvider { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IUserService UserService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IUmbracoContextAccessor UmbracoContextAccessor { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static ISiteDomainMapper SiteDomainHelper { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IExamineManager ExamineManager { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IFileService FileService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IOptions WebRoutingSettings { get; } = - StaticServiceProvider.Instance.GetRequiredService>(); - - private static IContentTypeService ContentTypeService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IPublishedValueFallback PublishedValueFallback { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - - private static IPublishedSnapshot? PublishedSnapshot + get { - get + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } - return umbracoContext.PublishedSnapshot; + return null; } + + return umbracoContext.PublishedSnapshot; } + } - private static IMediaTypeService MediaTypeService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); + private static IMediaTypeService MediaTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + private static IMemberTypeService MemberTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - private static IMemberTypeService MemberTypeService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); + /// + /// Creates a strongly typed published content model for an internal published content. + /// + /// The internal published content. + /// The strongly typed published content model. + public static IPublishedContent? CreateModel( + this IPublishedContent content) + => content.CreateModel(PublishedModelFactory); + /// + /// Gets the name of the content item. + /// + /// The content item. + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static string? Name( + this IPublishedContent content, + string? culture = null) + => content.Name(VariationContextAccessor, culture); - /// - /// Creates a strongly typed published content model for an internal published content. - /// - /// The internal published content. - /// The strongly typed published content model. - public static IPublishedContent? CreateModel( - this IPublishedContent content) - => content.CreateModel(PublishedModelFactory); + /// + /// Gets the URL segment of the content item. + /// + /// The content item. + /// + /// The specific culture to get the URL segment for. If null is used the current culture is used + /// (Default is null). + /// + public static string? UrlSegment( + this IPublishedContent content, + string? culture = null) + => content.UrlSegment(VariationContextAccessor, culture); - /// - /// Gets the name of the content item. - /// - /// The content item. - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static string? Name( - this IPublishedContent content, - string? culture = null) - => content.Name(VariationContextAccessor, culture); + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static DateTime CultureDate( + this IPublishedContent content, + string? culture = null) + => content.CultureDate(VariationContextAccessor, culture); - /// - /// Gets the URL segment of the content item. - /// - /// The content item. - /// The specific culture to get the URL segment for. If null is used the current culture is used (Default is null). - public static string? UrlSegment( - this IPublishedContent content, - string? culture = null) - => content.UrlSegment(VariationContextAccessor, culture); + /// + /// Returns the current template Alias + /// + /// Empty string if none is set. + public static string GetTemplateAlias(this IPublishedContent content) + => content.GetTemplateAlias(FileService); - /// - /// Gets the culture date of the content item. - /// - /// The content item. - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static DateTime CultureDate( - this IPublishedContent content, - string? culture = null) - => content.CultureDate(VariationContextAccessor, culture); + public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) + => content.IsAllowedTemplate(ContentTypeService, WebRoutingSettings.Value, templateId); - /// - /// Returns the current template Alias - /// - /// Empty string if none is set. - public static string GetTemplateAlias(this IPublishedContent content) - => content.GetTemplateAlias(FileService); + public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) + => content.IsAllowedTemplate( + WebRoutingSettings.Value.DisableAlternativeTemplates, + WebRoutingSettings.Value.ValidateAlternativeTemplates, + templateAlias); - public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) - => content.IsAllowedTemplate(ContentTypeService, WebRoutingSettings.Value, templateId); + public static bool IsAllowedTemplate( + this IPublishedContent content, + bool disableAlternativeTemplates, + bool validateAlternativeTemplates, + int templateId) + => content.IsAllowedTemplate( + ContentTypeService, + disableAlternativeTemplates, + validateAlternativeTemplates, + templateId); - public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) - => content.IsAllowedTemplate(WebRoutingSettings.Value.DisableAlternativeTemplates, WebRoutingSettings.Value.ValidateAlternativeTemplates, templateAlias); + public static bool IsAllowedTemplate( + this IPublishedContent content, + bool disableAlternativeTemplates, + bool validateAlternativeTemplates, + string templateAlias) + => content.IsAllowedTemplate( + FileService, + ContentTypeService, + disableAlternativeTemplates, + validateAlternativeTemplates, + templateAlias); - public static bool IsAllowedTemplate( - this IPublishedContent content, - bool disableAlternativeTemplates, - bool validateAlternativeTemplates, - int templateId) - => content.IsAllowedTemplate( - ContentTypeService, - disableAlternativeTemplates, - validateAlternativeTemplates, - templateId); - - public static bool IsAllowedTemplate( - this IPublishedContent content, - bool disableAlternativeTemplates, - bool validateAlternativeTemplates, - string templateAlias) - => content.IsAllowedTemplate( - FileService, - ContentTypeService, - disableAlternativeTemplates, - validateAlternativeTemplates, - templateAlias); - - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// A value indicating whether the content has a value for the property identified by the alias. - /// Returns true if HasValue is true, or a fallback strategy can provide a value. - public static bool HasValue( - this IPublishedContent content, - string alias, - string? culture = null, - string? segment = null, - Fallback fallback = default) - => + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if HasValue is true, or a fallback strategy can provide a value. + public static bool HasValue( + this IPublishedContent content, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default) + => content.HasValue(PublishedValueFallback, alias, culture, segment, fallback); - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - public static object? Value(this IPublishedContent content, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + public static object? Value(this IPublishedContent content, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - public static T? Value(this IPublishedContent content, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) - => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + public static T? Value(this IPublishedContent content, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelfOfType( - this IEnumerable parentNodes, string docTypeAlias, string? culture = null) - => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, docTypeAlias, culture); + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, string docTypeAlias, string? culture = null) + => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, docTypeAlias, culture); - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelf( - this IEnumerable parentNodes, - string? culture = null) - where T : class, IPublishedContent - => parentNodes.DescendantsOrSelf(VariationContextAccessor, culture); + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf( + this IEnumerable parentNodes, + string? culture = null) + where T : class, IPublishedContent + => parentNodes.DescendantsOrSelf(VariationContextAccessor, culture); - public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) - => content.Descendants(VariationContextAccessor, culture); + public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) + => content.Descendants(VariationContextAccessor, culture); - public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) - => content.Descendants(VariationContextAccessor, level, culture); + public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) + => content.Descendants(VariationContextAccessor, level, culture); - public static IEnumerable DescendantsOfType(this IPublishedContent content, - string contentTypeAlias, string? culture = null) - => content.DescendantsOfType(VariationContextAccessor, contentTypeAlias, culture); + public static IEnumerable DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.DescendantsOfType(VariationContextAccessor, contentTypeAlias, culture); - public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, culture); + public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.Descendants(VariationContextAccessor, culture); - public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) - where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, level, culture); + public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) + where T : class, IPublishedContent + => content.Descendants(VariationContextAccessor, level, culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + string? culture = null) + => content.DescendantsOrSelf(VariationContextAccessor, culture); + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) + => content.DescendantsOrSelf(VariationContextAccessor, level, culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.DescendantsOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); - public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantsOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(VariationContextAccessor, culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, culture); + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(VariationContextAccessor, level, culture); - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) - where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + public static IPublishedContent? Descendant(this IPublishedContent content, string? culture = null) + => content.Descendant(VariationContextAccessor, culture); - public static IPublishedContent? Descendant(this IPublishedContent content, string? culture = null) - => content.Descendant(VariationContextAccessor, culture); + public static IPublishedContent? Descendant(this IPublishedContent content, int level, string? culture = null) + => content.Descendant(VariationContextAccessor, level, culture); - public static IPublishedContent? Descendant(this IPublishedContent content, int level, string? culture = null) - => content.Descendant(VariationContextAccessor, level, culture); + public static IPublishedContent? DescendantOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.DescendantOfType(VariationContextAccessor, contentTypeAlias, culture); + public static T? Descendant(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.Descendant(VariationContextAccessor, culture); - public static IPublishedContent? DescendantOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOfType(VariationContextAccessor, contentTypeAlias, culture); + public static T? Descendant(this IPublishedContent content, int level, string? culture = null) + where T : class, IPublishedContent + => content.Descendant(VariationContextAccessor, level, culture); - public static T? Descendant(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, culture); + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string? culture = null) + => content.DescendantOrSelf(VariationContextAccessor, culture); + public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) + => content.DescendantOrSelf(VariationContextAccessor, level, culture); - public static T? Descendant(this IPublishedContent content, int level, string? culture = null) - where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, level, culture); + public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.DescendantOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); - public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string? culture = null) - => content.DescendantOrSelf(VariationContextAccessor, culture); + public static T? DescendantOrSelf(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.DescendantOrSelf(VariationContextAccessor, culture); - public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantOrSelf(VariationContextAccessor, level, culture); + public static T? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) + where T : class, IPublishedContent + => content.DescendantOrSelf(VariationContextAccessor, level, culture); - public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + /// + /// Gets the children of the content item. + /// + /// The content item. + /// + /// The specific culture to get the URL children for. Default is null which will use the current culture in + /// + /// + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the + /// Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant + /// children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// + /// + public static IEnumerable? Children(this IPublishedContent content, string? culture = null) + => content.Children(VariationContextAccessor, culture); - public static T? DescendantOrSelf(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, culture); + /// + /// Gets the children of the content, filtered by a predicate. + /// + /// The content. + /// The predicate. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content, filtered by the predicate. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children( + this IPublishedContent content, + Func predicate, + string? culture = null) + => content.Children(VariationContextAccessor, predicate, culture); - public static T? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) - where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, level, culture); + /// + /// Gets the children of the content, of any of the specified types. + /// + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The children of the content, of any of the specified types. + public static IEnumerable? ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.ChildrenOfType(VariationContextAccessor, contentTypeAlias, culture); + /// + /// Gets the children of the content, of a given content type. + /// + /// The content type. + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of content, of the given content type. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.Children(VariationContextAccessor, culture); - /// - /// Gets the children of the content item. - /// - /// The content item. - /// - /// The specific culture to get the URL children for. Default is null which will use the current culture in - /// - /// - /// Gets children that are available for the specified culture. - /// Children are sorted by their sortOrder. - /// - /// For culture, - /// if null is used the current culture is used. - /// If an empty string is used only invariant children are returned. - /// If "*" is used all children are returned. - /// - /// - /// If a variant culture is specified or there is a current culture in the then the Children returned - /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. - /// However, if an empty string is specified only invariant children are returned. - /// - /// - public static IEnumerable? Children(this IPublishedContent content, string? culture = null) - => content.Children(VariationContextAccessor, culture); + public static IPublishedContent? FirstChild(this IPublishedContent content, string? culture = null) + => content.FirstChild(VariationContextAccessor, culture); - /// - /// Gets the children of the content, filtered by a predicate. - /// - /// The content. - /// The predicate. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content, filtered by the predicate. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, Func predicate, string? culture = null) - => content.Children(VariationContextAccessor, predicate, culture); + /// + /// Gets the first child of the content, of a given content type. + /// + public static IPublishedContent? FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.FirstChildOfType(VariationContextAccessor, contentTypeAlias, culture); - /// - /// Gets the children of the content, of any of the specified types. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The children of the content, of any of the specified types. - public static IEnumerable? ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.ChildrenOfType(VariationContextAccessor, contentTypeAlias, culture); + public static IPublishedContent? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) + => content.FirstChild(VariationContextAccessor, predicate, culture); - /// - /// Gets the children of the content, of a given content type. - /// - /// The content type. - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of content, of the given content type. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.Children(VariationContextAccessor, culture); + public static IPublishedContent? FirstChild(this IPublishedContent content, Guid uniqueId, string? culture = null) + => content.FirstChild(VariationContextAccessor, uniqueId, culture); - public static IPublishedContent? FirstChild(this IPublishedContent content, string? culture = null) - => content.FirstChild(VariationContextAccessor, culture); + public static T? FirstChild(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.FirstChild(VariationContextAccessor, culture); - /// - /// Gets the first child of the content, of a given content type. - /// - public static IPublishedContent? FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.FirstChildOfType(VariationContextAccessor, contentTypeAlias, culture); + public static T? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) + where T : class, IPublishedContent + => content.FirstChild(VariationContextAccessor, predicate, culture); - public static IPublishedContent? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) - => content.FirstChild(VariationContextAccessor, predicate, culture); + /// + /// Gets the siblings of the content. + /// + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) + => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); - public static IPublishedContent? FirstChild(this IPublishedContent content, Guid uniqueId, string? culture = null) - => content.FirstChild(VariationContextAccessor, uniqueId, culture); + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content. + /// The content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) + => content.SiblingsOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content type. + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); - public static T? FirstChild(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, culture); + /// + /// Gets the siblings of the content including the node itself to indicate the position. + /// + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself. + public static IEnumerable? SiblingsAndSelf( + this IPublishedContent content, + string? culture = null) + => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); - public static T? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) - where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, predicate, culture); + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelfOfType( + this IPublishedContent content, + string contentTypeAlias, + string? culture = null) + => content.SiblingsAndSelfOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); - /// - /// Gets the siblings of the content. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) - => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content type. + /// The content. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, string? culture = null) + where T : class, IPublishedContent + => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content. - /// The content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + /// + /// Gets the url of the content item. + /// + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string Url(this IPublishedContent content, string? culture = null, UrlMode mode = UrlMode.Default) + => content.Url(PublishedUrlProvider, culture, mode); - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content type. - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// An optional content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content. + public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string? culture = null) + => + content.ChildrenAsTable( + VariationContextAccessor, + ContentTypeService, + MediaTypeService, + MemberTypeService, + PublishedUrlProvider, + contentTypeAliasFilter, + culture); - /// - /// Gets the siblings of the content including the node itself to indicate the position. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, string? culture = null) - => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + /// + /// Gets the url for a media. + /// + /// The content item. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string MediaUrl( + this IPublishedContent content, + string? culture = null, + UrlMode mode = UrlMode.Default, + string propertyAlias = Constants.Conventions.Media.File) + => content.MediaUrl(PublishedUrlProvider, culture, mode, propertyAlias); - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsAndSelfOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + /// + /// Gets the name of the content item creator. + /// + /// The content item. + public static string? CreatorName(this IPublishedContent content) => + content.CreatorName(UserService); - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content type. - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, string? culture = null) - where T : class, IPublishedContent - => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + /// + /// Gets the name of the content item writer. + /// + /// The content item. + public static string? WriterName(this IPublishedContent content) => + content.WriterName(UserService); + /// + /// Gets the culture assigned to a document by domains, in the context of a current Uri. + /// + /// The document. + /// An optional current Uri. + /// The culture assigned to the document by domains. + /// + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + /// + public static string? GetCultureFromDomains( + this IPublishedContent content, + Uri? current = null) + => content.GetCultureFromDomains(UmbracoContextAccessor, SiteDomainHelper, current); - /// - /// Gets the url of the content item. - /// - /// - /// If the content item is a document, then this method returns the url of the - /// document. If it is a media, then this methods return the media url for the - /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other - /// properties. - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string Url(this IPublishedContent content, string? culture = null, UrlMode mode = UrlMode.Default) - => content.Url(PublishedUrlProvider, culture, mode); + public static IEnumerable SearchDescendants( + this IPublishedContent content, + string term, + string? indexName = null) + => content.SearchDescendants(ExamineManager, UmbracoContextAccessor, term, indexName); - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string? culture = null) - => content.ChildrenAsTable(VariationContextAccessor, ContentTypeService, MediaTypeService, MemberTypeService, PublishedUrlProvider, contentTypeAliasFilter, culture); - - /// - /// Gets the url for a media. - /// - /// The content item. - /// The culture (use current culture by default). - /// The url mode (use site configuration by default). - /// The alias of the property (use 'umbracoFile' by default). - /// The url for the media. - /// - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string MediaUrl( - this IPublishedContent content, - string? culture = null, - UrlMode mode = UrlMode.Default, - string propertyAlias = Constants.Conventions.Media.File) - => content.MediaUrl(PublishedUrlProvider, culture, mode, propertyAlias); - - /// - /// Gets the name of the content item creator. - /// - /// The content item. - public static string? CreatorName(this IPublishedContent content) => - content.CreatorName(UserService); - - /// - /// Gets the name of the content item writer. - /// - /// The content item. - public static string? WriterName(this IPublishedContent content) => - content.WriterName(UserService); - - /// - /// Gets the culture assigned to a document by domains, in the context of a current Uri. - /// - /// The document. - /// An optional current Uri. - /// The culture assigned to the document by domains. - /// - /// In 1:1 multilingual setup, a document contains several cultures (there is not - /// one document per culture), and domains, withing the context of a current Uri, assign - /// a culture to that document. - /// - public static string? GetCultureFromDomains( - this IPublishedContent content, - Uri? current = null) - => content.GetCultureFromDomains(UmbracoContextAccessor, SiteDomainHelper, current); - - - public static IEnumerable SearchDescendants( - this IPublishedContent content, - string term, - string? indexName = null) - => content.SearchDescendants(ExamineManager, UmbracoContextAccessor, term, indexName); - - - public static IEnumerable SearchChildren( - this IPublishedContent content, - string term, - string? indexName = null) - => content.SearchChildren(ExamineManager, UmbracoContextAccessor, term, indexName); - - - } + public static IEnumerable SearchChildren( + this IPublishedContent content, + string term, + string? indexName = null) + => content.SearchChildren(ExamineManager, UmbracoContextAccessor, term, indexName); } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs index fe10a33837..61761475c5 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs @@ -1,82 +1,105 @@ -using System; using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FriendlyPublishedElementExtensions { - public static class FriendlyPublishedElementExtensions - { - private static IPublishedValueFallback PublishedValueFallback { get; } = - StaticServiceProvider.Instance.GetRequiredService(); + private static IPublishedValueFallback PublishedValueFallback { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - /// - /// Gets the value of a content's property identified by its alias. - /// - /// The content. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object? Value( - this IPublishedElement content, - string alias, - string? culture = null, - string? segment = null, - Fallback fallback = default, - object? defaultValue = default) - => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, returns + /// . + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static object? Value( + this IPublishedElement content, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + object? defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T? Value( - this IPublishedElement content, - string alias, - string? culture = null, - string? segment = null, - Fallback fallback = default, - T? defaultValue = default) - => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, or if it could not be + /// converted, returns default(T). + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static T? Value( + this IPublishedElement content, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + T? defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); - /// - /// Gets a value indicating whether the content is visible. - /// - /// The content. - /// A value indicating whether the content is visible. - /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, - /// the content is visible. - public static bool IsVisible(this IPublishedElement content) => content.IsVisible(PublishedValueFallback); + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// A value indicating whether the content is visible. + /// + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + /// + public static bool IsVisible(this IPublishedElement content) => content.IsVisible(PublishedValueFallback); - - /// - /// Gets the value of a property. - /// - public static TValue? ValueFor(this TModel model, Expression> property, string? culture = null, string? segment = null, Fallback fallback = default, TValue? defaultValue = default) - where TModel : IPublishedElement => - model.ValueFor(PublishedValueFallback, property, culture, segment, fallback); - } + /// + /// Gets the value of a property. + /// + public static TValue? ValueFor( + this TModel model, + Expression> property, + string? culture = null, + string? segment = null, + Fallback fallback = default, + TValue? defaultValue = default) + where TModel : IPublishedElement => + model.ValueFor(PublishedValueFallback, property, culture, segment, fallback); } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs index c812e77c01..a9da209ba3 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs @@ -1,50 +1,52 @@ -using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FriendlyUrlHelperExtensions { - public static class FriendlyUrlHelperExtensions - { + private static IUmbracoContext UmbracoContext => + StaticServiceProvider.Instance.GetRequiredService().GetRequiredUmbracoContext(); - private static IUmbracoContext UmbracoContext => - StaticServiceProvider.Instance.GetRequiredService().GetRequiredUmbracoContext(); + private static IDataProtectionProvider DataProtectionProvider { get; } = + StaticServiceProvider.Instance.GetRequiredService(); - private static IDataProtectionProvider DataProtectionProvider { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, string action, string controllerName) - => UrlHelperExtensions.SurfaceAction(url, UmbracoContext, DataProtectionProvider, action, controllerName); + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + /// + /// + /// + public static string SurfaceAction(this IUrlHelper url, string action, string controllerName) + => url.SurfaceAction(UmbracoContext, DataProtectionProvider, action, controllerName); - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, string action, string controllerName, object additionalRouteVals) - => UrlHelperExtensions.SurfaceAction(url, UmbracoContext, DataProtectionProvider, action, controllerName, additionalRouteVals); + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + /// + /// + /// + /// + public static string SurfaceAction(this IUrlHelper url, string action, string controllerName, object additionalRouteVals) + => url.SurfaceAction(UmbracoContext, DataProtectionProvider, action, controllerName, additionalRouteVals); - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, string action, string controllerName, string area, object additionalRouteVals) - => UrlHelperExtensions.SurfaceAction(url, UmbracoContext, DataProtectionProvider, action, controllerName, area, additionalRouteVals); - } + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + /// + /// + /// + /// + /// + public static string SurfaceAction(this IUrlHelper url, string action, string controllerName, string area, object additionalRouteVals) + => url.SurfaceAction(UmbracoContext, DataProtectionProvider, action, controllerName, area, additionalRouteVals); } diff --git a/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs index 223f418f22..2c959ab441 100644 --- a/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs @@ -1,114 +1,183 @@ -using System; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class GridTemplateExtensions { - public static class GridTemplateExtensions + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedProperty property, string framework = "bootstrap3") { - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedProperty property, string framework = "bootstrap3") + if (property.GetValue() is string asString && string.IsNullOrEmpty(asString)) { - var asString = property.GetValue() as string; - if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - - var view = "grid/" + framework; - return html.Partial(view, property.GetValue()); + return new HtmlString(string.Empty); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem) + var view = "grid/" + framework; + return html.Partial(view, property.GetValue()); + } + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem) => + html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) + { + if (propertyAlias == null) { - return html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + throw new ArgumentNullException(nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) + if (string.IsNullOrWhiteSpace(propertyAlias)) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string framework) + return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string framework) + { + if (propertyAlias == null) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - var view = "grid/" + framework; - var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); - var model = prop.GetValue(); - - var asString = model as string; - if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - - return html.Partial(view, model); + throw new ArgumentNullException(nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem) + if (string.IsNullOrWhiteSpace(propertyAlias)) { - return html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem, string propertyAlias) + var view = "grid/" + framework; + IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); + if (prop == null) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + throw new InvalidOperationException("No property type found with alias " + propertyAlias); } - public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem, string propertyAlias, string framework) + var model = prop.GetValue(); + + if (model is string asString && string.IsNullOrEmpty(asString)) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - var view = "grid/" + framework; - var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); - var model = prop.GetValue(); - - var asString = model as string; - if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - - return html.Partial(view, model); - } - public static IHtmlContent GetGridHtml(this IPublishedProperty property, IHtmlHelper html, string framework = "bootstrap3") - { - var asString = property.GetValue() as string; - if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - - var view = "grid/" + framework; - return html.Partial(view, property.GetValue()); + return new HtmlString(string.Empty); } - public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html) + return html.Partial(view, model); + } + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem) => + html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem, string propertyAlias) + { + if (propertyAlias == null) { - return GetGridHtml(contentItem, html, "bodyText", "bootstrap3"); + throw new ArgumentNullException(nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html, string propertyAlias) + if (string.IsNullOrWhiteSpace(propertyAlias)) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3"); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); } - public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html, string propertyAlias, string framework) + return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + public static IHtmlContent GetGridHtml(this IHtmlHelper html, IPublishedElement contentItem, string propertyAlias, string framework) + { + if (propertyAlias == null) { - if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - - var view = "grid/" + framework; - var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); - var model = prop.GetValue(); - - var asString = model as string; - if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - - return html.Partial(view, model); + throw new ArgumentNullException(nameof(propertyAlias)); } + + if (string.IsNullOrWhiteSpace(propertyAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); + } + + var view = "grid/" + framework; + IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); + if (prop == null) + { + throw new InvalidOperationException("No property type found with alias " + propertyAlias); + } + + var model = prop.GetValue(); + + if (model is string asString && string.IsNullOrEmpty(asString)) + { + return new HtmlString(string.Empty); + } + + return html.Partial(view, model); + } + + public static IHtmlContent GetGridHtml(this IPublishedProperty property, IHtmlHelper html, string framework = "bootstrap3") + { + if (property.GetValue() is string asString && string.IsNullOrEmpty(asString)) + { + return new HtmlString(string.Empty); + } + + var view = "grid/" + framework; + return html.Partial(view, property.GetValue()); + } + + public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html) => + GetGridHtml(contentItem, html, "bodyText", "bootstrap3"); + + public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html, string propertyAlias) + { + if (propertyAlias == null) + { + throw new ArgumentNullException(nameof(propertyAlias)); + } + + if (string.IsNullOrWhiteSpace(propertyAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); + } + + return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3"); + } + + public static IHtmlContent GetGridHtml(this IPublishedContent contentItem, IHtmlHelper html, string propertyAlias, string framework) + { + if (propertyAlias == null) + { + throw new ArgumentNullException(nameof(propertyAlias)); + } + + if (string.IsNullOrWhiteSpace(propertyAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(propertyAlias)); + } + + var view = "grid/" + framework; + IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); + if (prop == null) + { + throw new InvalidOperationException("No property type found with alias " + propertyAlias); + } + + var model = prop.GetValue(); + + if (model is string asString && string.IsNullOrEmpty(asString)) + { + return new HtmlString(string.Empty); + } + + return html.Partial(view, model); } } diff --git a/src/Umbraco.Web.Common/Extensions/HtmlContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/HtmlContentExtensions.cs index 3b74c39475..93c06ee0d8 100644 --- a/src/Umbraco.Web.Common/Extensions/HtmlContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HtmlContentExtensions.cs @@ -1,17 +1,16 @@ -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HtmlContentExtensions { - public static class HtmlContentExtensions + public static string ToHtmlString(this IHtmlContent content) { - public static string ToHtmlString(this IHtmlContent content) + using (var writer = new StringWriter()) { - using (var writer = new System.IO.StringWriter()) - { - content.WriteTo(writer, HtmlEncoder.Default); - return writer.ToString(); - } + content.WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); } } } diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs index e3d1e752f0..2c37f2e1b0 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs @@ -1,18 +1,23 @@ -using System; using Microsoft.AspNetCore.Http; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HttpContextAccessorExtensions { - public static class HttpContextAccessorExtensions + public static HttpContext GetRequiredHttpContext(this IHttpContextAccessor httpContextAccessor) { - public static HttpContext GetRequiredHttpContext(this IHttpContextAccessor httpContextAccessor) + if (httpContextAccessor == null) { - if (httpContextAccessor == null) throw new ArgumentNullException(nameof(httpContextAccessor)); - var httpContext = httpContextAccessor.HttpContext; - - if(httpContext is null) throw new InvalidOperationException("HttpContext is null"); - - return httpContext; + throw new ArgumentNullException(nameof(httpContextAccessor)); } + + HttpContext? httpContext = httpContextAccessor.HttpContext; + + if (httpContext is null) + { + throw new InvalidOperationException("HttpContext is null"); + } + + return httpContext; } } diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index d7ee7fd627..34af34fe45 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -1,112 +1,110 @@ -using System; using System.Security.Claims; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; +using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HttpContextExtensions { - public static class HttpContextExtensions + /// + /// Try to get the basic auth username and password from the http context. + /// + public static bool TryGetBasicAuthCredentials(this HttpContext httpContext, out string? username, out string? password) { - /// - /// Try to get the basic auth username and password from the http context. - /// - public static bool TryGetBasicAuthCredentials(this HttpContext httpContext, out string? username, out string? password) + username = null; + password = null; + + if (httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authHeaders)) { - username = null; - password = null; - - if (httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authHeaders)) + var authHeader = authHeaders.ToString(); + if (authHeader is not null && authHeader.StartsWith("Basic")) { - var authHeader = authHeaders.ToString(); - if (authHeader is not null && authHeader.StartsWith("Basic")) - { - // Extract credentials. - var encodedUsernamePassword = authHeader.Substring(6).Trim(); - Encoding encoding = Encoding.UTF8; - var usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword)); + // Extract credentials. + var encodedUsernamePassword = authHeader.Substring(6).Trim(); + Encoding encoding = Encoding.UTF8; + var usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword)); - var seperatorIndex = usernamePassword.IndexOf(':'); + var seperatorIndex = usernamePassword.IndexOf(':'); - username = usernamePassword.Substring(0, seperatorIndex); - password = usernamePassword.Substring(seperatorIndex + 1); - } - - return true; + username = usernamePassword.Substring(0, seperatorIndex); + password = usernamePassword.Substring(seperatorIndex + 1); } - return false; + return true; } - /// - /// Runs the authentication process - /// - public static async Task AuthenticateBackOfficeAsync(this HttpContext httpContext) - { - if (httpContext == null) - { - return AuthenticateResult.NoResult(); - } + return false; + } - var result = await httpContext.AuthenticateAsync(Cms.Core.Constants.Security.BackOfficeAuthenticationType); - return result; + /// + /// Runs the authentication process + /// + public static async Task AuthenticateBackOfficeAsync(this HttpContext? httpContext) + { + if (httpContext == null) + { + return AuthenticateResult.NoResult(); } - /// - /// Get the value in the request form or query string for the key - /// - public static string GetRequestValue(this HttpContext context, string key) - { - HttpRequest request = context.Request; - if (!request.HasFormContentType) - { - return request.Query[key]; - } + AuthenticateResult result = + await httpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); + return result; + } - string value = request.Form[key]; - return value ?? request.Query[key]; + /// + /// Get the value in the request form or query string for the key + /// + public static string GetRequestValue(this HttpContext context, string key) + { + HttpRequest request = context.Request; + if (!request.HasFormContentType) + { + return request.Query[key]; } - public static void SetPrincipalForRequest(this HttpContext context, ClaimsPrincipal? principal) + string value = request.Form[key]; + return value ?? request.Query[key]; + } + + public static void SetPrincipalForRequest(this HttpContext context, ClaimsPrincipal? principal) + { + if (principal is not null) { - if (principal is not null) - { - context.User = principal; - } - } - - - public static void SetReasonPhrase(this HttpContext httpContext, string? reasonPhrase) - { - //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body - // https://github.com/aspnet/HttpAbstractions/issues/395 - var httpResponseFeature = httpContext.Features.Get(); - if (!(httpResponseFeature is null)) - { - httpResponseFeature.ReasonPhrase = reasonPhrase; - } - } - - /// - /// This will return the current back office identity. - /// - /// - /// - /// Returns the current back office identity if an admin is authenticated otherwise null - /// - public static ClaimsIdentity? GetCurrentIdentity(this HttpContext http) - { - if (http == null) throw new ArgumentNullException(nameof(http)); - if (http.User == null) return null; //there's no user at all so no identity - - // If it's already a UmbracoBackOfficeIdentity - var backOfficeIdentity = http.User.GetUmbracoIdentity(); - if (backOfficeIdentity != null) return backOfficeIdentity; - - return null; + context.User = principal; } } + + public static void SetReasonPhrase(this HttpContext httpContext, string? reasonPhrase) + { + // TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body + // https://github.com/aspnet/HttpAbstractions/issues/395 + IHttpResponseFeature? httpResponseFeature = httpContext.Features.Get(); + if (!(httpResponseFeature is null)) + { + httpResponseFeature.ReasonPhrase = reasonPhrase; + } + } + + /// + /// This will return the current back office identity. + /// + /// + /// + /// Returns the current back office identity if an admin is authenticated otherwise null + /// + public static ClaimsIdentity? GetCurrentIdentity(this HttpContext http) + { + // If it's already a UmbracoBackOfficeIdentity + ClaimsIdentity? backOfficeIdentity = http.User.GetUmbracoIdentity(); + if (backOfficeIdentity != null) + { + return backOfficeIdentity; + } + + return null; + } } diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs index b511ccf8f1..f12e469b6b 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -1,161 +1,162 @@ -using System; -using System.IO; using System.Net; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for +/// +public static class HttpRequestExtensions { /// - /// Extension methods for + /// Check if a preview cookie exist /// - public static class HttpRequestExtensions + public static bool HasPreviewCookie(this HttpRequest request) + => request.Cookies.TryGetValue(Constants.Web.PreviewCookieName, out var cookieVal) && + !cookieVal.IsNullOrWhiteSpace(); + + /// + /// Returns true if the request is a back office request + /// + public static bool IsBackOfficeRequest(this HttpRequest request) { - /// - /// Check if a preview cookie exist - /// - public static bool HasPreviewCookie(this HttpRequest request) - => request.Cookies.TryGetValue(Cms.Core.Constants.Web.PreviewCookieName, out var cookieVal) && !cookieVal.IsNullOrWhiteSpace(); + PathString absPath = request.Path; + UmbracoRequestPaths? umbReqPaths = request.HttpContext.RequestServices.GetService(); + return umbReqPaths?.IsBackOfficeRequest(absPath) ?? false; + } - /// - /// Returns true if the request is a back office request - /// - public static bool IsBackOfficeRequest(this HttpRequest request) + /// + /// Returns true if the request is for a client side extension + /// + public static bool IsClientSideRequest(this HttpRequest request) + { + PathString absPath = request.Path; + UmbracoRequestPaths? umbReqPaths = request.HttpContext.RequestServices.GetService(); + return umbReqPaths?.IsClientSideRequest(absPath) ?? false; + } + + public static string? ClientCulture(this HttpRequest request) + => request.Headers.TryGetValue("X-UMB-CULTURE", out StringValues values) ? values[0] : null; + + /// + /// Determines if a request is local. + /// + /// True if request is local + /// + /// Hat-tip: https://stackoverflow.com/a/41242493/489433 + /// + public static bool IsLocal(this HttpRequest request) + { + ConnectionInfo connection = request.HttpContext.Connection; + if (connection.RemoteIpAddress?.IsSet() ?? false) { - PathString absPath = request.Path; - UmbracoRequestPaths? umbReqPaths = request.HttpContext.RequestServices.GetService(); - return umbReqPaths?.IsBackOfficeRequest(absPath) ?? false; + // We have a remote address set up + return connection.LocalIpAddress?.IsSet() ?? false + + // Is local is same as remote, then we are local + ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) + + // else we are remote if the remote IP address is not a loopback address + : IPAddress.IsLoopback(connection.RemoteIpAddress); } - /// - /// Returns true if the request is for a client side extension - /// - public static bool IsClientSideRequest(this HttpRequest request) + return true; + } + + public static string GetRawBodyString(this HttpRequest request, Encoding? encoding = null) + { + if (request.Body.CanSeek) { - PathString absPath = request.Path; - UmbracoRequestPaths? umbReqPaths = request.HttpContext.RequestServices.GetService(); - return umbReqPaths?.IsClientSideRequest(absPath) ?? false; + request.Body.Seek(0, SeekOrigin.Begin); } - public static string? ClientCulture(this HttpRequest request) - => request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null; - - /// - /// Determines if a request is local. - /// - /// True if request is local - /// - /// Hat-tip: https://stackoverflow.com/a/41242493/489433 - /// - public static bool IsLocal(this HttpRequest request) - { - var connection = request.HttpContext.Connection; - if (connection.RemoteIpAddress?.IsSet() ?? false) - { - // We have a remote address set up - return connection.LocalIpAddress?.IsSet() ?? false - // Is local is same as remote, then we are local - ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) - // else we are remote if the remote IP address is not a loopback address - : IPAddress.IsLoopback(connection.RemoteIpAddress); - } - - return true; - } - - private static bool IsSet(this IPAddress address) - { - const string NullIpAddress = "::1"; - return address != null && address.ToString() != NullIpAddress; - } - - public static string GetRawBodyString(this HttpRequest request, Encoding? encoding = null) + using (var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8, leaveOpen: true)) { + var result = reader.ReadToEnd(); if (request.Body.CanSeek) { request.Body.Seek(0, SeekOrigin.Begin); } - using (var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8, leaveOpen: true)) - { - var result = reader.ReadToEnd(); - if (request.Body.CanSeek) - { - request.Body.Seek(0, SeekOrigin.Begin); - } - - return result; - } - } - - public static async Task GetRawBodyStringAsync(this HttpRequest request, Encoding? encoding = null) - { - if (!request.Body.CanSeek) - { - request.EnableBuffering(); - } - - request.Body.Seek(0, SeekOrigin.Begin); - - using (var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8, leaveOpen: true)) - { - var result = await reader.ReadToEndAsync(); - request.Body.Seek(0, SeekOrigin.Begin); - - return result; - } - } - - /// - /// Gets the application URI, will use the one specified in settings if present - /// - public static Uri GetApplicationUri(this HttpRequest request, WebRoutingSettings routingSettings) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (routingSettings == null) - { - throw new ArgumentNullException(nameof(routingSettings)); - } - - if (string.IsNullOrEmpty(routingSettings.UmbracoApplicationUrl)) - { - var requestUri = new Uri(request.GetDisplayUrl()); - - // Create a new URI with the relative uri as /, this ensures that only the base path is returned. - return new Uri(requestUri, "/"); - } - - return new Uri(routingSettings.UmbracoApplicationUrl); - } - - /// - /// Gets the Umbraco `ufprt` encrypted string from the current request - /// - /// The current request - /// The extracted `ufprt` token. - public static string? GetUfprt(this HttpRequest request) - { - if (request.HasFormContentType && request.Form.TryGetValue("ufprt", out StringValues formVal) && formVal != StringValues.Empty) - { - return formVal.ToString(); - } - - if (request.Query.TryGetValue("ufprt", out StringValues queryVal) && queryVal != StringValues.Empty) - { - return queryVal.ToString(); - } - - return null; + return result; } } + + public static async Task GetRawBodyStringAsync(this HttpRequest request, Encoding? encoding = null) + { + if (!request.Body.CanSeek) + { + request.EnableBuffering(); + } + + request.Body.Seek(0, SeekOrigin.Begin); + + using (var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8, leaveOpen: true)) + { + var result = await reader.ReadToEndAsync(); + request.Body.Seek(0, SeekOrigin.Begin); + + return result; + } + } + + /// + /// Gets the application URI, will use the one specified in settings if present + /// + public static Uri GetApplicationUri(this HttpRequest request, WebRoutingSettings routingSettings) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (routingSettings == null) + { + throw new ArgumentNullException(nameof(routingSettings)); + } + + if (string.IsNullOrEmpty(routingSettings.UmbracoApplicationUrl)) + { + var requestUri = new Uri(request.GetDisplayUrl()); + + // Create a new URI with the relative uri as /, this ensures that only the base path is returned. + return new Uri(requestUri, "/"); + } + + return new Uri(routingSettings.UmbracoApplicationUrl); + } + + /// + /// Gets the Umbraco `ufprt` encrypted string from the current request + /// + /// The current request + /// The extracted `ufprt` token. + public static string? GetUfprt(this HttpRequest request) + { + if (request.HasFormContentType && request.Form.TryGetValue("ufprt", out StringValues formVal) && + formVal != StringValues.Empty) + { + return formVal.ToString(); + } + + if (request.Query.TryGetValue("ufprt", out StringValues queryVal) && queryVal != StringValues.Empty) + { + return queryVal.ToString(); + } + + return null; + } + + private static bool IsSet(this IPAddress address) + { + const string nullIpAddress = "::1"; + return address.ToString() != nullIpAddress; + } } diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index 5ec7adc1f3..d7d71d72e5 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -1,75 +1,83 @@ -using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Security; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for +/// +public static class IdentityBuilderExtensions { /// - /// Extension methods for + /// Adds a for the . /// - public static class IdentityBuilderExtensions + /// The member manager interface + /// The member manager type + /// The current instance. + public static IdentityBuilder AddMemberManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface + where TInterface : notnull { - /// - /// Adds a for the . - /// - /// The member manager interface - /// The member manager type - /// The current instance. - public static IdentityBuilder AddMemberManager(this IdentityBuilder identityBuilder) - where TUserManager : UserManager, TInterface - where TInterface : notnull - { - identityBuilder.AddUserManager(); - // use a UniqueServiceDescriptor so we can check if it's already been added - var memberManagerDescriptor = new UniqueServiceDescriptor(typeof(TInterface), typeof(TUserManager), ServiceLifetime.Scoped); - identityBuilder.Services.Add(memberManagerDescriptor); - identityBuilder.Services.AddScoped(typeof(UserManager), factory => factory.GetRequiredService()); - return identityBuilder; - } + identityBuilder.AddUserManager(); - public static IdentityBuilder AddRoleManager(this IdentityBuilder identityBuilder) - where TRoleManager : RoleManager, TInterface - where TInterface : notnull - { - identityBuilder.AddRoleManager(); - identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TRoleManager)); - identityBuilder.Services.AddScoped(typeof(RoleManager), factory => factory.GetRequiredService()); - return identityBuilder; - } + // use a UniqueServiceDescriptor so we can check if it's already been added + var memberManagerDescriptor = + new UniqueServiceDescriptor(typeof(TInterface), typeof(TUserManager), ServiceLifetime.Scoped); + identityBuilder.Services.Add(memberManagerDescriptor); + identityBuilder.Services.AddScoped( + typeof(UserManager), + factory => factory.GetRequiredService()); + return identityBuilder; + } - /// - /// Adds a implementation for - /// - /// The sign in manager interface - /// The sign in manager type - /// The - /// The current instance. - public static IdentityBuilder AddSignInManager(this IdentityBuilder identityBuilder) - where TSignInManager : SignInManager, TInterface - { - identityBuilder.AddSignInManager(); - identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager)); - return identityBuilder; - } + public static IdentityBuilder AddRoleManager(this IdentityBuilder identityBuilder) + where TRoleManager : RoleManager, TInterface + where TInterface : notnull + { + identityBuilder.AddRoleManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TRoleManager)); + identityBuilder.Services.AddScoped( + typeof(RoleManager), + factory => factory.GetRequiredService()); + return identityBuilder; + } + /// + /// Adds a implementation for + /// + /// The sign in manager interface + /// The sign in manager type + /// The + /// The current instance. + public static IdentityBuilder AddSignInManager(this IdentityBuilder identityBuilder) + where TSignInManager : SignInManager, TInterface + { + identityBuilder.AddSignInManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager)); + return identityBuilder; + } - public static IdentityBuilder AddUserStore(this IdentityBuilder identityBuilder, Func implementationFactory) - where TStore : class, TInterface - { - identityBuilder.Services.AddScoped(typeof(TInterface), implementationFactory); - return identityBuilder; - } + public static IdentityBuilder AddUserStore( + this IdentityBuilder identityBuilder, + Func implementationFactory) + where TStore : class, TInterface + { + identityBuilder.Services.AddScoped(typeof(TInterface), implementationFactory); + return identityBuilder; + } - public static MemberIdentityBuilder AddTwoFactorProvider(this MemberIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider - { - identityBuilder.Services.AddSingleton(); - identityBuilder.Services.AddSingleton(); - identityBuilder.AddTokenProvider>(providerName); + public static MemberIdentityBuilder AddTwoFactorProvider( + this MemberIdentityBuilder identityBuilder, + string providerName) + where T : class, ITwoFactorProvider + { + identityBuilder.Services.AddSingleton(); + identityBuilder.Services.AddSingleton(); + identityBuilder.AddTokenProvider>(providerName); - return identityBuilder; - } + return identityBuilder; } } diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index 57779d2082..78a01dca2d 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Globalization; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core; @@ -8,421 +7,572 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ImageCropperTemplateCoreExtensions { - public static class ImageCropperTemplateCoreExtensions + /// + /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias) on the + /// IPublishedContent item. + /// + /// The IPublishedContent item. + /// The crop alias e.g. thumbnail. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + cropAlias: cropAlias, + useCropDimensions: true, + urlMode: urlMode); + + /// + /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias in the + /// MediaWithCrops content item) on the MediaWithCrops item. + /// + /// The MediaWithCrops item. + /// The crop alias e.g. thumbnail. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this MediaWithCrops mediaWithCrops, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => + mediaWithCrops.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + cropAlias: cropAlias, + useCropDimensions: true, + urlMode: urlMode); + + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The url mode.s + /// + /// The image crop URL. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + ImageCropperValue imageCropperValue, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + imageCropperValue, + true, + cropAlias: cropAlias, + useCropDimensions: true, + urlMode: urlMode); + + /// + /// Gets the underlying image processing service URL by the crop alias using the specified property containing the + /// image cropper JSON data on the IPublishedContent item. + /// + /// The IPublishedContent item. + /// The property alias of the property containing the JSON data e.g. umbracoFile. + /// The crop alias e.g. thumbnail. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + string propertyAlias, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + propertyAlias: propertyAlias, + cropAlias: cropAlias, + useCropDimensions: true, + urlMode: urlMode); + + /// + /// Gets the underlying image processing service URL by the crop alias using the specified property containing the + /// image cropper JSON data on the MediaWithCrops content item. + /// + /// The MediaWithCrops item. + /// The property alias of the property containing the JSON data e.g. umbracoFile. + /// The crop alias e.g. thumbnail. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this MediaWithCrops mediaWithCrops, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + string propertyAlias, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + UrlMode urlMode = UrlMode.Default) => + mediaWithCrops.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + propertyAlias: propertyAlias, + cropAlias: cropAlias, + useCropDimensions: true, + urlMode: urlMode); + + /// + /// Gets the underlying image processing service URL from the IPublishedContent item. + /// + /// The IPublishedContent item. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The width of the output image. + /// The height of the output image. + /// Property alias of the property containing the JSON data. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point, to generate an output image using the focal point instead of the + /// predefined crop. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + null, + false, + width, + height, + propertyAlias, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBuster, + furtherOptions, + urlMode); + + /// + /// Gets the underlying image processing service URL from the MediaWithCrops item. + /// + /// The MediaWithCrops item. + /// The image URL generator. + /// The published value fallback. + /// The published URL provider. + /// The width of the output image. + /// The height of the output image. + /// Property alias of the property containing the JSON data. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point, to generate an output image using the focal point instead of the + /// predefined crop. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// The url mode. + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this MediaWithCrops mediaWithCrops, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + UrlMode urlMode = UrlMode.Default) { - /// - /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The crop alias e.g. thumbnail. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - string cropAlias, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - - /// - /// Gets the underlying image processing service URL by the crop alias (from the "umbracoFile" property alias in the MediaWithCrops content item) on the MediaWithCrops item. - /// - /// The MediaWithCrops item. - /// The crop alias e.g. thumbnail. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this MediaWithCrops mediaWithCrops, - string cropAlias, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - UrlMode urlMode = UrlMode.Default) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - - /// - /// Gets the crop URL by using only the specified . - /// - /// The media item. - /// The image cropper value. - /// The crop alias. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The url mode.s - /// - /// The image crop URL. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - ImageCropperValue imageCropperValue, - string cropAlias, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - - /// - /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The property alias of the property containing the JSON data e.g. umbracoFile. - /// The crop alias e.g. thumbnail. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - string propertyAlias, - string cropAlias, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - - /// - /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the MediaWithCrops content item. - /// - /// The MediaWithCrops item. - /// The property alias of the property containing the JSON data e.g. umbracoFile. - /// The crop alias e.g. thumbnail. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl(this MediaWithCrops mediaWithCrops, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - string propertyAlias, - string cropAlias, - IImageUrlGenerator imageUrlGenerator, - UrlMode urlMode = UrlMode.Default) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - - /// - /// Gets the underlying image processing service URL from the IPublishedContent item. - /// - /// The IPublishedContent item. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The width of the output image. - /// The height of the output image. - /// Property alias of the property containing the JSON data. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point, to generate an output image using the focal point instead of the predefined crop. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: - /// - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this IPublishedContent mediaItem, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - int? width = null, - int? height = null, - string propertyAlias = Cms.Core.Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); - - /// - /// Gets the underlying image processing service URL from the MediaWithCrops item. - /// - /// The MediaWithCrops item. - /// The image URL generator. - /// The published value fallback. - /// The published URL provider. - /// The width of the output image. - /// The height of the output image. - /// Property alias of the property containing the JSON data. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point, to generate an output image using the focal point instead of the predefined crop. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: - /// - /// The url mode. - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this MediaWithCrops mediaWithCrops, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - int? width = null, - int? height = null, - string propertyAlias = Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - UrlMode urlMode = UrlMode.Default) + if (mediaWithCrops == null) { - if (mediaWithCrops == null) - { - throw new ArgumentNullException(nameof(mediaWithCrops)); - } - - return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); + throw new ArgumentNullException(nameof(mediaWithCrops)); } - private static string? GetCropUrl( - this IPublishedContent mediaItem, - IImageUrlGenerator imageUrlGenerator, - IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider, - ImageCropperValue? localCrops, - bool localCropsOnly, - int? width = null, - int? height = null, - string propertyAlias = Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - UrlMode urlMode = UrlMode.Default) - { - if (mediaItem == null) - { - throw new ArgumentNullException(nameof(mediaItem)); - } + return mediaWithCrops.Content.GetCropUrl( + imageUrlGenerator, + publishedValueFallback, + publishedUrlProvider, + mediaWithCrops.LocalCrops, + false, + width, + height, + propertyAlias, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBuster, + furtherOptions, + urlMode); + } - if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + /// + /// Gets the underlying image processing service URL from the image path. + /// + /// The image URL. + /// The image URL generator. + /// The width of the output image. + /// The height of the output image. + /// The Json data from the Umbraco Core Image Cropper property editor. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point to generate an output image using the focal point instead of the + /// predefined crop if there is one. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string? imageCropperValue = null, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string? cacheBusterValue = null, + string? furtherOptions = null) + { + if (string.IsNullOrWhiteSpace(imageUrl)) + { + return null; + } + + ImageCropperValue? cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && + (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.DeserializeImageCropperValue(); + } + + return GetCropUrl( + imageUrl, + imageUrlGenerator, + cropDataSet, + width, + height, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBusterValue, + furtherOptions); + } + + /// + /// Gets the underlying image processing service URL from the image path. + /// + /// The image URL. + /// + /// The generator that will process all the options and the image URL to return a full + /// image URLs with all processing options appended. + /// + /// The crop data set. + /// The width of the output image. + /// The height of the output image. + /// The crop alias. + /// Quality percentage of the output image. + /// The image crop mode. + /// The image crop anchor. + /// + /// Use focal point to generate an output image using the focal point instead of the + /// predefined crop if there is one. + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop + /// sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when + /// updated. + /// + /// + /// These are any query string parameters (formatted as query strings) that the underlying image processing service + /// supports. For example: + /// + /// + /// + /// The URL of the cropped image. + /// + public static string? GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + ImageCropperValue? cropDataSet, + int? width = null, + int? height = null, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string? cacheBusterValue = null, + string? furtherOptions = null) + { + if (string.IsNullOrWhiteSpace(imageUrl)) + { + return null; + } + + ImageUrlGenerationOptions options; + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + ImageCropperValue.ImageCropperCrop? crop = cropDataSet.GetCrop(cropAlias); + + // If a crop was specified, but not found, return null + if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) { return null; } - var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias, mode: urlMode); + options = cropDataSet.GetCropBaseOptions(imageUrl, crop, preferFocalPoint || string.IsNullOrWhiteSpace(cropAlias)); - // Only get crops from media when required and used - if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + if (crop != null && useCropDimensions) { - // Get the default cropper value from the value converter - var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias); - - var mediaCrops = cropperValue as ImageCropperValue; - - if (mediaCrops == null && cropperValue is JObject jobj) - { - mediaCrops = jobj.ToObject(); - } - - if (mediaCrops == null && cropperValue is string imageCropperValue && - string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson()) - { - mediaCrops = imageCropperValue.DeserializeImageCropperValue(); - } - - // Merge crops - if (localCrops == null) - { - localCrops = mediaCrops; - } - else if (mediaCrops != null) - { - localCrops = localCrops.Merge(mediaCrops); - } + width = crop.Width; + height = crop.Height; } - var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - - return GetCropUrl( - mediaItemUrl, imageUrlGenerator, localCrops, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions); + // Calculate missing dimension if a predefined crop has been specified, but has no coordinates + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null) + { + if (width != null && height == null) + { + height = (int)MathF.Round(width.Value * ((float)crop.Height / crop.Width)); + } + else if (width == null && height != null) + { + width = (int)MathF.Round(height.Value * ((float)crop.Width / crop.Height)); + } + } } - - /// - /// Gets the underlying image processing service URL from the image path. - /// - /// The image URL. - /// The image URL generator. - /// The width of the output image. - /// The height of the output image. - /// The Json data from the Umbraco Core Image Cropper property editor. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this string imageUrl, - IImageUrlGenerator imageUrlGenerator, - int? width = null, - int? height = null, - string? imageCropperValue = null, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - string? cacheBusterValue = null, - string? furtherOptions = null) + else { - if (string.IsNullOrWhiteSpace(imageUrl)) + options = new ImageUrlGenerationOptions(imageUrl) { - return null; - } - - ImageCropperValue? cropDataSet = null; - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - cropDataSet = imageCropperValue.DeserializeImageCropperValue(); - } - - return GetCropUrl( - imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions); + ImageCropMode = imageCropMode ?? ImageCropMode.Pad, // Not sure why we default to Pad + ImageCropAnchor = imageCropAnchor, + }; } - /// - /// Gets the underlying image processing service URL from the image path. - /// - /// The image URL. - /// The generator that will process all the options and the image URL to return a full image URLs with all processing options appended. - /// The crop data set. - /// The width of the output image. - /// The height of the output image. - /// The crop alias. - /// Quality percentage of the output image. - /// The image crop mode. - /// The image crop anchor. - /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one. - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. - /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated. - /// These are any query string parameters (formatted as query strings) that the underlying image processing service supports. For example: - /// - /// - /// The URL of the cropped image. - /// - public static string? GetCropUrl( - this string imageUrl, - IImageUrlGenerator imageUrlGenerator, - ImageCropperValue? cropDataSet, - int? width = null, - int? height = null, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - string? cacheBusterValue = null, - string? furtherOptions = null) + options.Quality = quality; + options.Width = width; + options.Height = height; + options.FurtherOptions = furtherOptions; + options.CacheBusterValue = cacheBusterValue; + + return imageUrlGenerator.GetImageUrl(options); + } + + private static string? GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + ImageCropperValue? localCrops, + bool localCropsOnly, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + UrlMode urlMode = UrlMode.Default) + { + if (mediaItem == null) { - if (string.IsNullOrWhiteSpace(imageUrl)) - { - return null; - } - - ImageUrlGenerationOptions options; - if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - ImageCropperValue.ImageCropperCrop? crop = cropDataSet.GetCrop(cropAlias); - - // If a crop was specified, but not found, return null - if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) - { - return null; - } - - options = cropDataSet.GetCropBaseOptions(imageUrl, crop, preferFocalPoint || string.IsNullOrWhiteSpace(cropAlias)); - - if (crop != null && useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // Calculate missing dimension if a predefined crop has been specified, but has no coordinates - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null) - { - if (width != null && height == null) - { - height = (int)MathF.Round(width.Value * ((float)crop.Height / crop.Width)); - } - else if (width == null && height != null) - { - width = (int)MathF.Round(height.Value * ((float)crop.Width / crop.Height)); - } - } - } - else - { - options = new ImageUrlGenerationOptions(imageUrl) - { - ImageCropMode = (imageCropMode ?? ImageCropMode.Pad), // Not sure why we default to Pad - ImageCropAnchor = imageCropAnchor - }; - } - - options.Quality = quality; - options.Width = width; - options.Height = height; - options.FurtherOptions = furtherOptions; - options.CacheBusterValue = cacheBusterValue; - - return imageUrlGenerator.GetImageUrl(options); + throw new ArgumentNullException(nameof(mediaItem)); } + + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + { + return null; + } + + var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias, mode: urlMode); + + // Only get crops from media when required and used + if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + // Get the default cropper value from the value converter + var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias); + + var mediaCrops = cropperValue as ImageCropperValue; + + if (mediaCrops == null && cropperValue is JObject jobj) + { + mediaCrops = jobj.ToObject(); + } + + if (mediaCrops == null && cropperValue is string imageCropperValue && + string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson()) + { + mediaCrops = imageCropperValue.DeserializeImageCropperValue(); + } + + // Merge crops + if (localCrops == null) + { + localCrops = mediaCrops; + } + else if (mediaCrops != null) + { + localCrops = localCrops.Merge(mediaCrops); + } + } + + var cacheBusterValue = + cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; + + return GetCropUrl( + mediaItemUrl, + imageUrlGenerator, + localCrops, + width, + height, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBusterValue, + furtherOptions); } } diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs index e7b08c51f1..fdd9966fac 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs @@ -1,43 +1,40 @@ -using System; using System.Globalization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Umbraco.Cms.Core; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for getting ImageProcessor URL from the core Image Cropper property editor +/// +public static class ImageCropperTemplateExtensions { - /// - /// Provides extension methods for getting ImageProcessor URL from the core Image Cropper property editor - /// - public static class ImageCropperTemplateExtensions + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new() { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal, + }; - private static readonly JsonSerializerSettings s_imageCropperValueJsonSerializerSettings = new JsonSerializerSettings + internal static ImageCropperValue DeserializeImageCropperValue(this string json) + { + ImageCropperValue? imageCrops = null; + + if (json.DetectIsJson()) { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }; - - internal static ImageCropperValue DeserializeImageCropperValue(this string json) - { - ImageCropperValue? imageCrops = null; - - if (json.DetectIsJson()) + try { - try - { - imageCrops = JsonConvert.DeserializeObject(json, s_imageCropperValueJsonSerializerSettings); - } - catch (Exception ex) - { - StaticApplicationLogging.Logger.LogError(ex, "Could not parse the json string: {Json}", json); - } + imageCrops = + JsonConvert.DeserializeObject(json, ImageCropperValueJsonSerializerSettings); + } + catch (Exception ex) + { + StaticApplicationLogging.Logger.LogError(ex, "Could not parse the json string: {Json}", json); } - - imageCrops ??= new ImageCropperValue(); - return imageCrops; - } + + imageCrops ??= new ImageCropperValue(); + return imageCrops; } } diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 627778eb2f..ce6e8c7816 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Dynamic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.AspNetCore.Mvc; @@ -11,160 +8,173 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Web.Mvc; using Umbraco.Cms.Web.Common.Controllers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class LinkGeneratorExtensions { - public static class LinkGeneratorExtensions + /// + /// Return the back office url if the back office is installed + /// + public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) { - /// - /// Return the back office url if the back office is installed - /// - public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + Type? backOfficeControllerType; + try { - - Type? backOfficeControllerType; - try + backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice") + .GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); + if (backOfficeControllerType == null) { - backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); - if (backOfficeControllerType == null) - { - return "/"; // this would indicate that the installer is installed without the back office - } + return "/"; // this would indicate that the installer is installed without the back office } - catch - { - return hostingEnvironment.ApplicationVirtualPath; // this would indicate that the installer is installed without the back office - } - - return linkGenerator.GetPathByAction("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), values: new { area = Cms.Core.Constants.Web.Mvc.BackOfficeApiArea }); + } + catch + { + return + hostingEnvironment + .ApplicationVirtualPath; // this would indicate that the installer is installed without the back office } - /// - /// Return the Url for a Web Api service - /// - /// The - public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object? id = null) - where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( - actionName, - typeof(T), - new Dictionary() - { - ["id"] = id - }); + return linkGenerator.GetPathByAction( + "Default", + ControllerExtensions.GetControllerName(backOfficeControllerType), + new { area = Constants.Web.Mvc.BackOfficeApiArea }); + } - public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary? values) - where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); + /// + /// Return the Url for a Web Api service + /// + /// The + public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object? id = null) + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary { ["id"] = id }); - public static string? GetUmbracoApiServiceBaseUrl(this LinkGenerator linkGenerator, Expression> methodSelector) - where T : UmbracoApiControllerBase + public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary? values) + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); + + public static string? GetUmbracoApiServiceBaseUrl( + this LinkGenerator linkGenerator, + Expression> methodSelector) + where T : UmbracoApiControllerBase + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) { - var method = ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - return linkGenerator.GetUmbracoApiService(method.Name)?.TrimEnd(method.Name); + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); } - /// - /// Return the Url for an Umbraco controller - /// - public static string? GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string? area, IDictionary? dict = null) + return linkGenerator.GetUmbracoApiService(method.Name)?.TrimEnd(method.Name); + } + + /// + /// Return the Url for an Umbraco controller + /// + public static string? GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string? area, IDictionary? dict = null) + { + if (actionName == null) { - if (actionName == null) - { - throw new ArgumentNullException(nameof(actionName)); - } - - if (string.IsNullOrWhiteSpace(actionName)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - } - - if (controllerName == null) - { - throw new ArgumentNullException(nameof(controllerName)); - } - - if (string.IsNullOrWhiteSpace(controllerName)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); - } - - if (dict is null) - { - dict = new Dictionary(); - } - - if (!area.IsNullOrWhiteSpace()) - { - dict["area"] = area!; - } - - IDictionary values = dict.Aggregate( - new ExpandoObject() as IDictionary, - (a, p) => - { - a.Add(p.Key, p.Value); - return a; - }); - - return linkGenerator.GetPathByAction(actionName, controllerName, values); + throw new ArgumentNullException(nameof(actionName)); } - /// - /// Return the Url for an Umbraco controller - /// - public static string? GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary? values = null) + if (string.IsNullOrWhiteSpace(actionName)) { - if (actionName == null) - { - throw new ArgumentNullException(nameof(actionName)); - } - - if (string.IsNullOrWhiteSpace(actionName)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - } - - if (controllerType == null) - { - throw new ArgumentNullException(nameof(controllerType)); - } - - var area = string.Empty; - - if (!typeof(ControllerBase).IsAssignableFrom(controllerType)) - { - throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}"); - } - - PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType); - if (metaData.AreaName.IsNullOrWhiteSpace() == false) - { - // set the area to the plugin area - area = metaData.AreaName; - } - - return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); } - public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, Expression> methodSelector) - where T : UmbracoApiController + if (controllerName == null) { - var method = ExpressionHelper.GetMethodInfo(methodSelector); - var methodParams = ExpressionHelper.GetMethodParams(methodSelector); - if (method == null) - { - throw new MissingMethodException( - $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); - } - - if (methodParams?.Any() == false) - { - return linkGenerator.GetUmbracoApiService(method.Name); - } - - return linkGenerator.GetUmbracoApiService(method.Name, methodParams); + throw new ArgumentNullException(nameof(controllerName)); } + + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(controllerName)); + } + + if (dict is null) + { + dict = new Dictionary(); + } + + if (!area.IsNullOrWhiteSpace()) + { + dict["area"] = area!; + } + + IDictionary values = dict.Aggregate( + new ExpandoObject() as IDictionary, + (a, p) => + { + a.Add(p.Key, p.Value); + return a; + }); + + return linkGenerator.GetPathByAction(actionName, controllerName, values); + } + + /// + /// Return the Url for an Umbraco controller + /// + public static string? GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary? values = null) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (controllerType == null) + { + throw new ArgumentNullException(nameof(controllerType)); + } + + var area = string.Empty; + + if (!typeof(ControllerBase).IsAssignableFrom(controllerType)) + { + throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}"); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType); + if (metaData.AreaName.IsNullOrWhiteSpace() == false) + { + // set the area to the plugin area + area = metaData.AreaName; + } + + return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); + } + + public static string? GetUmbracoApiService( + this LinkGenerator linkGenerator, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary? methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); + } + + if (methodParams?.Any() == false) + { + return linkGenerator.GetUmbracoApiService(method.Name); + } + + return linkGenerator.GetUmbracoApiService(method.Name, methodParams); } } diff --git a/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs index 7509761305..8f01654c29 100644 --- a/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs @@ -1,55 +1,53 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Umbraco.Extensions -{ - public static class ModelStateExtensions - { - /// - /// Checks if there are any model errors on any fields containing the prefix - /// - /// - /// - /// - public static bool IsValid(this ModelStateDictionary state, string prefix) => - state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value?.Errors.Any() ?? false); +namespace Umbraco.Extensions; - public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) +public static class ModelStateExtensions +{ + /// + /// Checks if there are any model errors on any fields containing the prefix + /// + /// + /// + /// + public static bool IsValid(this ModelStateDictionary state, string prefix) => + state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value?.Errors.Any() ?? false); + + public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) + { + var modelStateError = new Dictionary(); + foreach (KeyValuePair keyModelStatePair in modelState) { - var modelStateError = new Dictionary(); - foreach (KeyValuePair keyModelStatePair in modelState) + var key = keyModelStatePair.Key; + ModelErrorCollection errors = keyModelStatePair.Value.Errors; + if (errors.Count > 0) { - var key = keyModelStatePair.Key; - ModelErrorCollection errors = keyModelStatePair.Value.Errors; - if (errors != null && errors.Count > 0) - { - modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); - } + modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); } - return modelStateError; } - /// - /// Serializes the ModelState to JSON for JavaScript to interrogate the errors - /// - /// - /// - public static JsonResult? ToJsonErrors(this ModelStateDictionary state) => - new JsonResult(new - { - success = state.IsValid.ToString().ToLower(), - failureType = "ValidationError", - validationErrors = from e in state - where e.Value.Errors.Count > 0 - select new - { - name = e.Key, - errors = e.Value.Errors.Select(x => x.ErrorMessage) - .Concat( - e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception!.Message)) - } - }); + return modelStateError; } + + /// + /// Serializes the ModelState to JSON for JavaScript to interrogate the errors + /// + /// + /// + public static JsonResult ToJsonErrors(this ModelStateDictionary state) => + new(new + { + success = state.IsValid.ToString().ToLower(), + failureType = "ValidationError", + validationErrors = from e in state + where e.Value.Errors.Count > 0 + select new + { + name = e.Key, + errors = e.Value.Errors.Select(x => x.ErrorMessage) + .Concat( + e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception!.Message)), + }, + }); } diff --git a/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs index abe0a99730..c15e9fbfad 100644 --- a/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PasswordConfigurationExtensions { - public static class PasswordConfigurationExtensions + public static void ConfigurePasswordOptions(this PasswordOptions output, IPasswordConfiguration input) { - public static void ConfigurePasswordOptions(this PasswordOptions output, IPasswordConfiguration input) - { - output.RequiredLength = input.RequiredLength; - output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit; - output.RequireDigit = input.RequireDigit; - output.RequireLowercase = input.RequireLowercase; - output.RequireUppercase = input.RequireUppercase; - } + output.RequiredLength = input.RequiredLength; + output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit; + output.RequireDigit = input.RequireDigit; + output.RequireLowercase = input.RequireLowercase; + output.RequireUppercase = input.RequireUppercase; } } diff --git a/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs index 28ba55c320..8e5efa079c 100644 --- a/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; using System.Web; using Examine; +using Examine.Search; using Microsoft.AspNetCore.Html; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; @@ -10,212 +9,253 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PublishedContentExtensions { - public static class PublishedContentExtensions + #region Variations + + /// + /// Gets the culture assigned to a document by domains, in the context of a current Uri. + /// + /// The document. + /// + /// + /// An optional current Uri. + /// The culture assigned to the document by domains. + /// + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + /// + public static string? GetCultureFromDomains( + this IPublishedContent content, + IUmbracoContextAccessor umbracoContextAccessor, + ISiteDomainMapper siteDomainHelper, + Uri? current = null) { - #region Creator/Writer Names - - /// - /// Gets the name of the content item creator. - /// - /// The content item. - /// - public static string? CreatorName(this IPublishedContent content, IUserService userService) => userService.GetProfileById(content.CreatorId)?.Name; - - /// - /// Gets the name of the content item writer. - /// - /// The content item. - /// - public static string? WriterName(this IPublishedContent content, IUserService userService) => userService.GetProfileById(content.WriterId)?.Name; - - #endregion - - #region Variations - - /// - /// Gets the culture assigned to a document by domains, in the context of a current Uri. - /// - /// The document. - /// - /// - /// An optional current Uri. - /// The culture assigned to the document by domains. - /// - /// In 1:1 multilingual setup, a document contains several cultures (there is not - /// one document per culture), and domains, withing the context of a current Uri, assign - /// a culture to that document. - /// - public static string? GetCultureFromDomains(this IPublishedContent content, IUmbracoContextAccessor umbracoContextAccessor, ISiteDomainMapper siteDomainHelper, Uri? current = null) - { - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper); - } - - #endregion - - #region Search - - public static IEnumerable SearchDescendants(this IPublishedContent content, IExamineManager examineManager, IUmbracoContextAccessor umbracoContextAccessor, string term, string ?indexName = null) - { - indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; - if (!examineManager.TryGetIndex(indexName, out var index)) - { - throw new InvalidOperationException("No index found with name " + indexName); - } - - //var t = term.Escape().Value; - //var luceneQuery = "+__Path:(" + content.Path.Replace("-", "\\-") + "*) +" + t; - - var query = index.Searcher.CreateQuery() - .Field(UmbracoExamineFieldNames.IndexPathFieldName, (content.Path + ",").MultipleCharacterWildcard()) - .And() - .ManagedQuery(term); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - return query.Execute().ToPublishedSearchResults(umbracoContext.Content); - } - - public static IEnumerable SearchChildren(this IPublishedContent content, IExamineManager examineManager, IUmbracoContextAccessor umbracoContextAccessor, string term, string? indexName = null) - { - indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; - if (!examineManager.TryGetIndex(indexName, out var index)) - { - throw new InvalidOperationException("No index found with name " + indexName); - } - - //var t = term.Escape().Value; - //var luceneQuery = "+parentID:" + content.Id + " +" + t; - - var query = index.Searcher.CreateQuery() - .Field("parentID", content.Id) - .And() - .ManagedQuery(term); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - - return query.Execute().ToPublishedSearchResults(umbracoContext.Content); - } - - #endregion - - #region IsSomething: equality - - /// - /// If the specified is equal to , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsEqual(other, valueIfTrue, string.Empty); - - /// - /// If the specified is equal to , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsEqual(other) ? valueIfTrue : valueIfFalse)); - - /// - /// If the specified is not equal to , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsNotEqual(other, valueIfTrue, string.Empty); - - /// - /// If the specified is not equal to , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsNotEqual(other) ? valueIfTrue : valueIfFalse)); - - #endregion - - #region IsSomething: ancestors and descendants - - /// - /// If the specified is a decendant of , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsDescendant(other, valueIfTrue, string.Empty); - - /// - /// If the specified is a decendant of , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsDescendant(other) ? valueIfTrue : valueIfFalse)); - - public static IHtmlContent IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsDescendantOrSelf(other, valueIfTrue, string.Empty); - - /// - /// If the specified is a decendant of or are the same, the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsDescendantOrSelf(other) ? valueIfTrue : valueIfFalse)); - - - public static IHtmlContent IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsAncestor(other, valueIfTrue, string.Empty); - - /// - /// If the specified is an ancestor of , the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsAncestor(other) ? valueIfTrue : valueIfFalse)); - - public static IHtmlContent IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsAncestorOrSelf(other, valueIfTrue, string.Empty); - - /// - /// If the specified is an ancestor of or are the same, the HTML encoded will be returned; otherwise, . - /// - /// The content. - /// The other content. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => new HtmlString(HttpUtility.HtmlEncode(content.IsAncestorOrSelf(other) ? valueIfTrue : valueIfFalse)); - - #endregion + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper); } + + #endregion + + #region Creator/Writer Names + + /// + /// Gets the name of the content item creator. + /// + /// The content item. + /// + public static string? CreatorName(this IPublishedContent content, IUserService userService) => + userService.GetProfileById(content.CreatorId)?.Name; + + /// + /// Gets the name of the content item writer. + /// + /// The content item. + /// + public static string? WriterName(this IPublishedContent content, IUserService userService) => + userService.GetProfileById(content.WriterId)?.Name; + + #endregion + + #region Search + + public static IEnumerable SearchDescendants( + this IPublishedContent content, + IExamineManager examineManager, + IUmbracoContextAccessor umbracoContextAccessor, + string term, + string? indexName = null) + { + indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; + if (!examineManager.TryGetIndex(indexName, out IIndex? index)) + { + throw new InvalidOperationException("No index found with name " + indexName); + } + + // var t = term.Escape().Value; + // var luceneQuery = "+__Path:(" + content.Path.Replace("-", "\\-") + "*) +" + t; + IBooleanOperation? query = index.Searcher.CreateQuery() + .Field(UmbracoExamineFieldNames.IndexPathFieldName, (content.Path + ",").MultipleCharacterWildcard()) + .And() + .ManagedQuery(term); + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + return query.Execute().ToPublishedSearchResults(umbracoContext.Content); + } + + public static IEnumerable SearchChildren( + this IPublishedContent content, + IExamineManager examineManager, + IUmbracoContextAccessor umbracoContextAccessor, + string term, + string? indexName = null) + { + indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; + if (!examineManager.TryGetIndex(indexName, out IIndex? index)) + { + throw new InvalidOperationException("No index found with name " + indexName); + } + + // var t = term.Escape().Value; + // var luceneQuery = "+parentID:" + content.Id + " +" + t; + IBooleanOperation? query = index.Searcher.CreateQuery() + .Field("parentID", content.Id) + .And() + .ManagedQuery(term); + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + + return query.Execute().ToPublishedSearchResults(umbracoContext.Content); + } + + #endregion + + #region IsSomething: equality + + /// + /// If the specified is equal to , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => + content.IsEqual(other, valueIfTrue, string.Empty); + + /// + /// If the specified is equal to , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsEqual( + this IPublishedContent content, + IPublishedContent other, + string valueIfTrue, + string valueIfFalse) => + new HtmlString(HttpUtility.HtmlEncode(content.IsEqual(other) ? valueIfTrue : valueIfFalse)); + + /// + /// If the specified is not equal to , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// + /// The HTML encoded value. + /// + public static IHtmlContent + IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => + content.IsNotEqual(other, valueIfTrue, string.Empty); + + /// + /// If the specified is not equal to , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => + new HtmlString(HttpUtility.HtmlEncode(content.IsNotEqual(other) ? valueIfTrue : valueIfFalse)); + + #endregion + + #region IsSomething: ancestors and descendants + + /// + /// If the specified is a decendant of , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// + /// The HTML encoded value. + /// + public static IHtmlContent + IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => + content.IsDescendant(other, valueIfTrue, string.Empty); + + /// + /// If the specified is a decendant of , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => + new HtmlString(HttpUtility.HtmlEncode(content.IsDescendant(other) ? valueIfTrue : valueIfFalse)); + + public static IHtmlContent IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => content.IsDescendantOrSelf(other, valueIfTrue, string.Empty); + + /// + /// If the specified is a decendant of or are the same, the HTML + /// encoded will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => + new HtmlString(HttpUtility.HtmlEncode(content.IsDescendantOrSelf(other) ? valueIfTrue : valueIfFalse)); + + public static IHtmlContent + IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue) => + content.IsAncestor(other, valueIfTrue, string.Empty); + + /// + /// If the specified is an ancestor of , the HTML encoded + /// will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) => + new HtmlString(HttpUtility.HtmlEncode(content.IsAncestor(other) ? valueIfTrue : valueIfFalse)); + + public static IHtmlContent IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) + => content.IsAncestorOrSelf(other, valueIfTrue, string.Empty); + + /// + /// If the specified is an ancestor of or are the same, the HTML + /// encoded will be returned; otherwise, . + /// + /// The content. + /// The other content. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) + => new HtmlString(HttpUtility.HtmlEncode(content.IsAncestorOrSelf(other) ? valueIfTrue : valueIfFalse)); + + #endregion } diff --git a/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs index 41d069919a..d02184d3ce 100644 --- a/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs @@ -1,24 +1,22 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Razor; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for +/// +public static class RazorPageExtensions { /// - /// Extension methods for + /// Renders a section with default content if the section isn't defined /// - public static class RazorPageExtensions - { - /// - /// Renders a section with default content if the section isn't defined - /// - public static HtmlString? RenderSection(this RazorPage webPage, string name, HtmlString defaultContents) - => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents; + public static HtmlString? RenderSection(this RazorPage webPage, string name, HtmlString defaultContents) + => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents; - /// - /// Renders a section with default content if the section isn't defined - /// - public static HtmlString? RenderSection(this RazorPage webPage, string name, string defaultContents) - => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents); - - } + /// + /// Renders a section with default content if the section isn't defined + /// + public static HtmlString? RenderSection(this RazorPage webPage, string name, string defaultContents) + => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents); } diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index 4c9d7d6c26..9a9d606644 100644 --- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -7,6 +6,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; +using Serilog.Core; using Serilog.Extensions.Hosting; using Serilog.Extensions.Logging; using Umbraco.Cms.Core.Cache; @@ -16,192 +16,192 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Logging.Serilog; -using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Cms.Infrastructure.Logging.Serilog; -using Umbraco.Cms.Web.Common.Logging.Enrichers; -using Umbraco.Cms.Core; +using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Cms.Web.Common.Logging; +using Umbraco.Cms.Web.Common.Logging.Enrichers; +using Constants = Umbraco.Cms.Core.Constants; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; using ILogger = Serilog.ILogger; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ServiceCollectionExtensions { - public static partial class ServiceCollectionExtensions + /// + /// Create and configure the logger + /// + [Obsolete("Use the extension method that takes an IHostEnvironment instance instead.")] + public static IServiceCollection AddLogger( + this IServiceCollection services, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) { - /// - /// Create and configure the logger - /// - [Obsolete("Use the extension method that takes an IHostEnvironment instance instead.")] - public static IServiceCollection AddLogger( - this IServiceCollection services, - IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) + // Create a serilog logger + var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out UmbracoFileConfiguration umbracoFileConfig); + services.AddSingleton(umbracoFileConfig); + + // This is nessasary to pick up all the loggins to MS ILogger. + Log.Logger = logger.SerilogLog; + + // Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since + // we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which + // is the only other option that these ext methods allow. + // I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that. + // Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs + services.AddLogging(configure => { - // Create a serilog logger - var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out var umbracoFileConfig); - services.AddSingleton(umbracoFileConfig); + configure.AddSerilog(logger.SerilogLog); + }); - // This is nessasary to pick up all the loggins to MS ILogger. - Log.Logger = logger.SerilogLog; + // This won't (and shouldn't) take ownership of the logger. + services.AddSingleton(logger.SerilogLog); - // Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since - // we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which - // is the only other option that these ext methods allow. - // I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that. - // Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs + // Registered to provide two services... + var diagnosticContext = new DiagnosticContext(logger.SerilogLog); - services.AddLogging(configure => - { - configure.AddSerilog(logger.SerilogLog, false); - }); + // Consumed by e.g. middleware + services.AddSingleton(diagnosticContext); - // This won't (and shouldn't) take ownership of the logger. - services.AddSingleton(logger.SerilogLog); + // Consumed by user code + services.AddSingleton(diagnosticContext); + services.AddSingleton(loggingConfiguration); - // Registered to provide two services... - var diagnosticContext = new DiagnosticContext(logger.SerilogLog); + return services; + } - // Consumed by e.g. middleware - services.AddSingleton(diagnosticContext); + /// + /// Create and configure the logger. + /// + /// + /// Additional Serilog services are registered during . + /// + public static IServiceCollection AddLogger( + this IServiceCollection services, + IHostEnvironment hostEnvironment, + IConfiguration configuration) + { + // TODO: WEBSITE_RUN_FROM_PACKAGE - can't assume this DIR is writable - we have an IConfiguration instance so a later refactor should be easy enough. + var loggingDir = hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles); + ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir); - // Consumed by user code - services.AddSingleton(diagnosticContext); - services.AddSingleton(loggingConfiguration); + var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); - return services; - } + services.TryAddSingleton(umbracoFileConfiguration); + services.TryAddSingleton(loggingConfig); + services.TryAddSingleton(); - /// - /// Create and configure the logger. - /// - /// - /// Additional Serilog services are registered during . - /// - public static IServiceCollection AddLogger( - this IServiceCollection services, - IHostEnvironment hostEnvironment, - IConfiguration configuration) + /////////////////////////////////////////////// + // Bootstrap logger setup + /////////////////////////////////////////////// + + LoggerConfiguration serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration) + .ReadFrom.Configuration(configuration); + + Log.Logger = serilogConfig.CreateBootstrapLogger(); + + /////////////////////////////////////////////// + // Runtime logger setup + /////////////////////////////////////////////// + + services.AddSingleton(sp => { - // TODO: WEBSITE_RUN_FROM_PACKAGE - can't assume this DIR is writable - we have an IConfiguration instance so a later refactor should be easy enough. - var loggingDir = hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles); - ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir); + var logger = new RegisteredReloadableLogger(Log.Logger as ReloadableLogger); - var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); - - services.TryAddSingleton(umbracoFileConfiguration); - services.TryAddSingleton(loggingConfig); - services.TryAddSingleton(); - - /////////////////////////////////////////////// - // Bootstrap logger setup - /////////////////////////////////////////////// - - LoggerConfiguration serilogConfig = new LoggerConfiguration() - .MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration) - .ReadFrom.Configuration(configuration); - - Log.Logger = serilogConfig.CreateBootstrapLogger(); - - /////////////////////////////////////////////// - // Runtime logger setup - /////////////////////////////////////////////// - - services.AddSingleton(sp => + logger.Reload(cfg => { - var logger = new RegisteredReloadableLogger(Log.Logger as ReloadableLogger); + cfg.MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration) + .ReadFrom.Configuration(configuration) + .ReadFrom.Services(sp); - logger.Reload(cfg => - { - cfg.MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration) - .ReadFrom.Configuration(configuration) - .ReadFrom.Services(sp); - - return cfg; - }); - - return logger; + return cfg; }); - services.AddSingleton(sp => - { - ILogger logger = sp.GetRequiredService().Logger; - return logger.ForContext(new NoopEnricher()); - }); + return logger; + }); - services.AddSingleton(sp => - { - ILogger logger = sp.GetRequiredService().Logger; - return new SerilogLoggerFactory(logger, false); - }); - - // Registered to provide two services... - var diagnosticContext = new DiagnosticContext(Log.Logger); - - // Consumed by e.g. middleware - services.TryAddSingleton(diagnosticContext); - - // Consumed by user code - services.TryAddSingleton(diagnosticContext); - - return services; - } - - /// - /// Called to create the to assign to the - /// - /// - /// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests. - /// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not - /// be the instance in DI. - /// - [Obsolete("Please use alternative extension method.")] - public static TypeLoader AddTypeLoader( - this IServiceCollection services, - Assembly entryAssembly, - IHostingEnvironment hostingEnvironment, - ILoggerFactory loggerFactory, - AppCaches appCaches, - IConfiguration configuration, - IProfiler profiler) => - services.AddTypeLoader(entryAssembly, loggerFactory, configuration); - - /// - /// Called to create the to assign to the - /// - /// - /// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests. - /// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not - /// be the instance in DI. - /// - public static TypeLoader AddTypeLoader( - this IServiceCollection services, - Assembly? entryAssembly, - ILoggerFactory loggerFactory, - IConfiguration configuration) + services.AddSingleton(sp => { - TypeFinderSettings typeFinderSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigTypeFinder).Get() ?? new TypeFinderSettings(); + ILogger logger = sp.GetRequiredService().Logger; + return logger.ForContext(new NoopEnricher()); + }); - var assemblyProvider = new DefaultUmbracoAssemblyProvider( - entryAssembly, - loggerFactory, - typeFinderSettings.AdditionalEntryAssemblies); + services.AddSingleton(sp => + { + ILogger logger = sp.GetRequiredService().Logger; + return new SerilogLoggerFactory(logger); + }); - var typeFinderConfig = new TypeFinderConfig(Options.Create(typeFinderSettings)); + // Registered to provide two services... + var diagnosticContext = new DiagnosticContext(Log.Logger); - var typeFinder = new TypeFinder( - loggerFactory.CreateLogger(), - assemblyProvider, - typeFinderConfig); + // Consumed by e.g. middleware + services.TryAddSingleton(diagnosticContext); - var typeLoader = new TypeLoader(typeFinder, loggerFactory.CreateLogger()); + // Consumed by user code + services.TryAddSingleton(diagnosticContext); - // This will add it ONCE and not again which is what we want since we don't actually want people to call this method - // in the web project. - services.TryAddSingleton(typeFinder); - services.TryAddSingleton(typeLoader); + return services; + } - return typeLoader; - } + /// + /// Called to create the to assign to the + /// + /// + /// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests. + /// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not + /// be the instance in DI. + /// + [Obsolete("Please use alternative extension method.")] + public static TypeLoader AddTypeLoader( + this IServiceCollection services, + Assembly entryAssembly, + IHostingEnvironment hostingEnvironment, + ILoggerFactory loggerFactory, + AppCaches appCaches, + IConfiguration configuration, + IProfiler profiler) => + services.AddTypeLoader(entryAssembly, loggerFactory, configuration); + + /// + /// Called to create the to assign to the + /// + /// + /// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests. + /// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not + /// be the instance in DI. + /// + public static TypeLoader AddTypeLoader( + this IServiceCollection services, + Assembly? entryAssembly, + ILoggerFactory loggerFactory, + IConfiguration configuration) + { + TypeFinderSettings typeFinderSettings = + configuration.GetSection(Constants.Configuration.ConfigTypeFinder).Get() ?? + new TypeFinderSettings(); + + var assemblyProvider = new DefaultUmbracoAssemblyProvider( + entryAssembly, + loggerFactory, + typeFinderSettings.AdditionalEntryAssemblies); + + var typeFinderConfig = new TypeFinderConfig(Options.Create(typeFinderSettings)); + + var typeFinder = new TypeFinder( + loggerFactory.CreateLogger(), + assemblyProvider, + typeFinderConfig); + + var typeLoader = new TypeLoader(typeFinder, loggerFactory.CreateLogger()); + + // This will add it ONCE and not again which is what we want since we don't actually want people to call this method + // in the web project. + services.TryAddSingleton(typeFinder); + services.TryAddSingleton(typeLoader); + + return typeLoader; } } diff --git a/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs index f8d682d76b..423ea52536 100644 --- a/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Web.Common.Controllers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeLoaderExtensions { - public static class TypeLoaderExtensions - { - /// - /// Gets all types implementing . - /// - public static IEnumerable GetUmbracoApiControllers(this TypeLoader typeLoader) - => typeLoader.GetTypes(); - } + /// + /// Gets all types implementing . + /// + public static IEnumerable GetUmbracoApiControllers(this TypeLoader typeLoader) + => typeLoader.GetTypes(); } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs index e7c0246f40..ec3f0f5055 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs @@ -1,52 +1,50 @@ -using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static partial class UmbracoApplicationBuilderExtensions { - public static partial class UmbracoApplicationBuilderExtensions + public static IUmbracoBuilder SetBackOfficeUserManager(this IUmbracoBuilder builder) + where TUserManager : UserManager, IBackOfficeUserManager { - public static IUmbracoBuilder SetBackOfficeUserManager(this IUmbracoBuilder builder) - where TUserManager : UserManager, IBackOfficeUserManager - { + Type customType = typeof(TUserManager); + Type userManagerType = typeof(UserManager); + builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IBackOfficeUserManager), customType)); + builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType)); + builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); + return builder; + } - Type customType = typeof(TUserManager); - Type userManagerType = typeof(UserManager); - builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IBackOfficeUserManager), customType)); - builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType)); - builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); - return builder; - } + public static IUmbracoBuilder SetBackOfficeUserStore(this IUmbracoBuilder builder) + where TUserStore : BackOfficeUserStore + { + Type customType = typeof(TUserStore); + builder.Services.Replace( + ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(BackOfficeIdentityUser)), customType)); + return builder; + } - public static IUmbracoBuilder SetBackOfficeUserStore(this IUmbracoBuilder builder) - where TUserStore : BackOfficeUserStore - { - Type customType = typeof(TUserStore); - builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(BackOfficeIdentityUser)), customType)); - return builder; - } + public static IUmbracoBuilder SetMemberManager(this IUmbracoBuilder builder) + where TUserManager : UserManager, IMemberManager + { + Type customType = typeof(TUserManager); + Type userManagerType = typeof(UserManager); + builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IMemberManager), customType)); + builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType)); + builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); + return builder; + } - public static IUmbracoBuilder SetMemberManager(this IUmbracoBuilder builder) - where TUserManager : UserManager, IMemberManager - { - - Type customType = typeof(TUserManager); - Type userManagerType = typeof(UserManager); - builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IMemberManager), customType)); - builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType)); - builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); - return builder; - } - - public static IUmbracoBuilder SetMemberUserStore(this IUmbracoBuilder builder) - where TUserStore : MemberUserStore - { - Type customType = typeof(TUserStore); - builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(MemberIdentityUser)), customType)); - return builder; - } + public static IUmbracoBuilder SetMemberUserStore(this IUmbracoBuilder builder) + where TUserStore : MemberUserStore + { + Type customType = typeof(TUserStore); + builder.Services.Replace( + ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(MemberIdentityUser)), customType)); + return builder; } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs index 4a7b0cf6de..1dcb73dd5d 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -1,32 +1,30 @@ -using System; using Smidge; using Smidge.Nuglify; using Umbraco.Cms.Web.Common.ApplicationBuilder; -using Umbraco.Extensions; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static partial class UmbracoApplicationBuilderExtensions { - public static partial class UmbracoApplicationBuilderExtensions + /// + /// Enables runtime minification for Umbraco + /// + public static IUmbracoEndpointBuilderContext UseUmbracoRuntimeMinificationEndpoints( + this IUmbracoEndpointBuilderContext app) { - /// - /// Enables runtime minification for Umbraco - /// - public static IUmbracoEndpointBuilderContext UseUmbracoRuntimeMinificationEndpoints(this IUmbracoEndpointBuilderContext app) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.RuntimeState.UmbracoCanBoot()) - { - return app; - } - - app.AppBuilder.UseSmidge(); - app.AppBuilder.UseSmidgeNuglify(); + throw new ArgumentNullException(nameof(app)); + } + if (!app.RuntimeState.UmbracoCanBoot()) + { return app; } + + app.AppBuilder.UseSmidge(); + app.AppBuilder.UseSmidgeNuglify(); + + return app; } } diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index 237fce8f0a..6eacc2ef24 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -1,7 +1,6 @@ -using System; using System.Globalization; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; @@ -14,319 +13,430 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Core.Web.Mvc; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UrlHelperExtensions { - public static class UrlHelperExtensions + /// + /// Return the back office url if the back office is installed + /// + /// + /// + public static string? GetBackOfficeUrl(this IUrlHelper url) { - /// - /// Return the back office url if the back office is installed - /// - /// - /// - public static string? GetBackOfficeUrl(this IUrlHelper url) + var backOfficeControllerType = Type.GetType("Umbraco.Web.BackOffice.Controllers"); + if (backOfficeControllerType == null) { - var backOfficeControllerType = Type.GetType("Umbraco.Web.BackOffice.Controllers"); - if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office - return url.Action("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Cms.Core.Constants.Web.Mvc.BackOfficeApiArea }); + return "/"; // this would indicate that the installer is installed without the back office } - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - /// - public static string? GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName, object? id = null) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, typeof(T), id); - } - - public static string? GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Expression> methodSelector) - where T : UmbracoApiController - { - var method = ExpressionHelper.GetMethodInfo(methodSelector); - var methodParams = ExpressionHelper.GetMethodParams(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - - if (methodParams?.Any() == false) - { - return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name); - } - return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name, methodParams?.Values.First()); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - /// - public static string? GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName, Type apiControllerType, object? id = null) - { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); - - var area = ""; - - var apiController = umbracoApiControllerTypeCollection.SingleOrDefault(x => x == apiControllerType); - if (apiController == null) - throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); - var metaData = PluginController.GetMetadata(apiController); - if (metaData.AreaName.IsNullOrWhiteSpace() == false) - { - //set the area to the plugin area - area = metaData.AreaName; - } - return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area!, id); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string? GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, object? id = null) - { - return url.GetUmbracoApiService(actionName, controllerName, "", id); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - /// - public static string? GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, string area, object? id = null) - { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); - - if (area.IsNullOrWhiteSpace()) - { - if (id == null) - { - return url.Action(actionName, controllerName); - } - else - { - return url.Action(actionName, controllerName, new { id = id }); - } - } - else - { - if (id == null) - { - return url.Action(actionName, controllerName, new { area = area }); - } - else - { - return url.Action(actionName, controllerName, new { area = area, id = id }); - } - } - } - - /// - /// Return the Base Url (not including the action) for a Web Api service - /// - /// - /// - /// - /// - /// - public static string? GetUmbracoApiServiceBaseUrl(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName)?.TrimEnd(actionName); - } - - public static string? GetUmbracoApiServiceBaseUrl(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Expression> methodSelector) - where T : UmbracoApiController - { - var method = ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name)?.TrimEnd(method.Name); - } - - /// - /// Return the Url for an action with a cache-busting hash appended - /// - /// - /// - /// - /// - /// - public static string GetUrlWithCacheBust(this IUrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals, - IHostingEnvironment hostingEnvironment, IUmbracoVersion umbracoVersion, IRuntimeMinifier runtimeMinifier) - { - var applicationJs = url.Action(actionName, controllerName, routeVals); - applicationJs = applicationJs + "?umb__rnd=" + GetCacheBustHash(hostingEnvironment, umbracoVersion, runtimeMinifier); - return applicationJs; - } - - /// - /// - /// - /// - public static string GetCacheBustHash(IHostingEnvironment hostingEnvironment, IUmbracoVersion umbracoVersion, IRuntimeMinifier runtimeMinifier) - { - //make a hash of umbraco and client dependency version - //in case the user bypasses the installer and just bumps the web.config or client dependency config - - //if in debug mode, always burst the cache - if (hostingEnvironment.IsDebugMode) - { - return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); - } - - var version = umbracoVersion.SemanticVersion.ToSemanticString(); - return $"{version}.{runtimeMinifier.CacheBuster}".GenerateHash(); - } - - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) - { - if (mediaItem == null) - { - return HtmlString.Empty; - } - - var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - return CreateHtmlString(url, htmlEncode); - } - - private static IHtmlContent CreateHtmlString(string? url, bool htmlEncode) => htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); - - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) - { - if (mediaItem == null) - { - return HtmlString.Empty; - } - - var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); - return CreateHtmlString(url, htmlEncode); - } - - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, - IPublishedContent mediaItem, - int? width = null, - int? height = null, - string propertyAlias = Constants.Conventions.Media.File, - string? cropAlias = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = false, - bool cacheBuster = true, - string? furtherOptions = null, - bool htmlEncode = true, - UrlMode urlMode = UrlMode.Default) - { - if (mediaItem == null) - { - return HtmlString.Empty; - } - - var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); - - return CreateHtmlString(url, htmlEncode); - } - - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, - ImageCropperValue imageCropperValue, - string cropAlias, - int? width = null, - int? height = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = true, - string? cacheBusterValue = null, - string? furtherOptions = null, - bool htmlEncode = true) - { - if (imageCropperValue == null) return HtmlString.Empty; - - var imageUrl = imageCropperValue.Src; - var url = imageUrl?.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions); - - return CreateHtmlString(url, htmlEncode); - } - - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, IUmbracoContext umbracoContext, IDataProtectionProvider dataProtectionProvider,string action, string controllerName) - { - return url.SurfaceAction(umbracoContext, dataProtectionProvider, action, controllerName, null); - } - - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, IUmbracoContext umbracoContext, IDataProtectionProvider dataProtectionProvider,string action, string controllerName, object? additionalRouteVals) - { - return url.SurfaceAction(umbracoContext, dataProtectionProvider, action, controllerName, "", additionalRouteVals); - } - - /// - /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController - /// - /// - /// - /// - /// - /// - /// - public static string SurfaceAction(this IUrlHelper url, IUmbracoContext umbracoContext, IDataProtectionProvider dataProtectionProvider, string action, string controllerName, string area, object? additionalRouteVals) - { - if (action == null) throw new ArgumentNullException(nameof(action)); - if (string.IsNullOrEmpty(action)) throw new ArgumentException("Value can't be empty.", nameof(action)); - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); - - var encryptedRoute = EncryptionHelper.CreateEncryptedRouteString(dataProtectionProvider, controllerName, action, area, additionalRouteVals); - - var result = umbracoContext.OriginalRequestUrl.AbsolutePath.EnsureEndsWith('?') + "ufprt=" + encryptedRoute; - return result; - } + return url.Action("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Constants.Web.Mvc.BackOfficeApiArea }); } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName, + object? id = null) + where T : UmbracoApiController => + url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, typeof(T), id); + + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary? methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); + } + + if (methodParams?.Any() == false) + { + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name); + } + + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name, methodParams?.Values.First()); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName, + Type apiControllerType, + object? id = null) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (apiControllerType == null) + { + throw new ArgumentNullException(nameof(apiControllerType)); + } + + var area = string.Empty; + + Type? apiController = umbracoApiControllerTypeCollection.SingleOrDefault(x => x == apiControllerType); + if (apiController == null) + { + throw new InvalidOperationException("Could not find the umbraco api controller of type " + + apiControllerType.FullName); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(apiController); + if (metaData.AreaName.IsNullOrWhiteSpace() == false) + { + // set the area to the plugin area + area = metaData.AreaName; + } + + return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area!, id); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, object? id = null) => url.GetUmbracoApiService(actionName, controllerName, string.Empty, id); + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + string actionName, + string controllerName, + string area, + object? id = null) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(controllerName)); + } + + if (area.IsNullOrWhiteSpace()) + { + if (id == null) + { + return url.Action(actionName, controllerName); + } + + return url.Action(actionName, controllerName, new { id }); + } + + if (id == null) + { + return url.Action(actionName, controllerName, new { area }); + } + + return url.Action(actionName, controllerName, new { area, id }); + } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiServiceBaseUrl( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName) + where T : UmbracoApiController => + url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName)?.TrimEnd(actionName); + + public static string? GetUmbracoApiServiceBaseUrl( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); + } + + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name)?.TrimEnd(method.Name); + } + + /// + /// Return the Url for an action with a cache-busting hash appended + /// + /// + public static string GetUrlWithCacheBust( + this IUrlHelper url, + string actionName, + string controllerName, + RouteValueDictionary routeVals, + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion, + IRuntimeMinifier runtimeMinifier) + { + var applicationJs = url.Action(actionName, controllerName, routeVals); + applicationJs = applicationJs + "?umb__rnd=" + + GetCacheBustHash(hostingEnvironment, umbracoVersion, runtimeMinifier); + return applicationJs; + } + + /// + /// + /// + public static string GetCacheBustHash(IHostingEnvironment hostingEnvironment, IUmbracoVersion umbracoVersion, IRuntimeMinifier runtimeMinifier) + { + // make a hash of umbraco and client dependency version + // in case the user bypasses the installer and just bumps the web.config or client dependency config + + // if in debug mode, always burst the cache + if (hostingEnvironment.IsDebugMode) + { + return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); + } + + var version = umbracoVersion.SemanticVersion.ToSemanticString(); + return $"{version}.{runtimeMinifier.CacheBuster}".GenerateHash(); + } + + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent? mediaItem, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent? mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlContent GetCropUrl( + this IUrlHelper urlHelper, + IPublishedContent? mediaItem, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string? cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string? furtherOptions = null, + bool htmlEncode = true, + UrlMode urlMode = UrlMode.Default) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl( + width, + height, + propertyAlias, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBuster, + furtherOptions, + urlMode); + + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlContent GetCropUrl( + this IUrlHelper urlHelper, + ImageCropperValue? imageCropperValue, + string cropAlias, + int? width = null, + int? height = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = true, + string? cacheBusterValue = null, + string? furtherOptions = null, + bool htmlEncode = true) + { + if (imageCropperValue == null) + { + return HtmlString.Empty; + } + + var imageUrl = imageCropperValue.Src; + var url = imageUrl?.GetCropUrl( + imageCropperValue, + width, + height, + cropAlias, + quality, + imageCropMode, + imageCropAnchor, + preferFocalPoint, + useCropDimensions, + cacheBusterValue, + furtherOptions); + + return CreateHtmlString(url, htmlEncode); + } + + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + public static string SurfaceAction( + this IUrlHelper url, + IUmbracoContext umbracoContext, + IDataProtectionProvider dataProtectionProvider, + string action, + string controllerName) => + url.SurfaceAction(umbracoContext, dataProtectionProvider, action, controllerName, null); + + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + public static string SurfaceAction( + this IUrlHelper url, + IUmbracoContext umbracoContext, + IDataProtectionProvider dataProtectionProvider, + string action, + string controllerName, + object? additionalRouteVals) => + url.SurfaceAction( + umbracoContext, + dataProtectionProvider, + action, + controllerName, + string.Empty, + additionalRouteVals); + + /// + /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified + /// SurfaceController + /// + /// + public static string SurfaceAction( + this IUrlHelper url, + IUmbracoContext umbracoContext, + IDataProtectionProvider dataProtectionProvider, + string action, + string controllerName, + string area, + object? additionalRouteVals) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (string.IsNullOrEmpty(action)) + { + throw new ArgumentException("Value can't be empty.", nameof(action)); + } + + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + + if (string.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException("Value can't be empty.", nameof(controllerName)); + } + + var encryptedRoute = EncryptionHelper.CreateEncryptedRouteString(dataProtectionProvider, controllerName, action, + area, additionalRouteVals); + + var result = umbracoContext.OriginalRequestUrl.AbsolutePath.EnsureEndsWith('?') + "ufprt=" + encryptedRoute; + return result; + } + + private static IHtmlContent CreateHtmlString(string? url, bool htmlEncode) => + htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } diff --git a/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs index 17a9d048fc..3cd62dea07 100644 --- a/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs @@ -1,75 +1,67 @@ -using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ViewContextExtensions { - public static class ViewContextExtensions - { - /// - /// Creates a new ViewContext from an existing one but specifies a new Model for the ViewData - /// - /// - /// - /// - public static ViewContext CopyWithModel(this ViewContext vc, object model) + /// + /// Creates a new ViewContext from an existing one but specifies a new Model for the ViewData + /// + /// + /// + /// + public static ViewContext CopyWithModel(this ViewContext vc, object model) => + new ViewContext { - return new ViewContext - { - View = vc.View, - Writer = vc.Writer, - ActionDescriptor = vc.ActionDescriptor, - FormContext = vc.FormContext, - HttpContext = vc.HttpContext, - RouteData = vc.RouteData, - TempData = vc.TempData, - ViewData = new ViewDataDictionary(vc.ViewData) - { - Model = model - }, - ClientValidationEnabled = vc.ClientValidationEnabled, - ExecutingFilePath = vc.ExecutingFilePath, - ValidationMessageElement = vc.ValidationMessageElement, - Html5DateRenderingMode = vc.Html5DateRenderingMode, - ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement - }; - } + View = vc.View, + Writer = vc.Writer, + ActionDescriptor = vc.ActionDescriptor, + FormContext = vc.FormContext, + HttpContext = vc.HttpContext, + RouteData = vc.RouteData, + TempData = vc.TempData, + ViewData = new ViewDataDictionary(vc.ViewData) { Model = model }, + ClientValidationEnabled = vc.ClientValidationEnabled, + ExecutingFilePath = vc.ExecutingFilePath, + ValidationMessageElement = vc.ValidationMessageElement, + Html5DateRenderingMode = vc.Html5DateRenderingMode, + ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement, + }; - public static ViewContext Clone(this ViewContext vc) + public static ViewContext Clone(this ViewContext vc) => + new ViewContext { - return new ViewContext - { - View = vc.View, - Writer = vc.Writer, - ActionDescriptor = vc.ActionDescriptor, - FormContext = vc.FormContext, - HttpContext = vc.HttpContext, - RouteData = vc.RouteData, - TempData = vc.TempData, - ViewData = new ViewDataDictionary(vc.ViewData), - ClientValidationEnabled = vc.ClientValidationEnabled, - ExecutingFilePath = vc.ExecutingFilePath, - ValidationMessageElement = vc.ValidationMessageElement, - Html5DateRenderingMode = vc.Html5DateRenderingMode, - ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement - }; - } + View = vc.View, + Writer = vc.Writer, + ActionDescriptor = vc.ActionDescriptor, + FormContext = vc.FormContext, + HttpContext = vc.HttpContext, + RouteData = vc.RouteData, + TempData = vc.TempData, + ViewData = new ViewDataDictionary(vc.ViewData), + ClientValidationEnabled = vc.ClientValidationEnabled, + ExecutingFilePath = vc.ExecutingFilePath, + ValidationMessageElement = vc.ValidationMessageElement, + Html5DateRenderingMode = vc.Html5DateRenderingMode, + ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement, + }; - //public static ViewContext CloneWithWriter(this ViewContext vc, TextWriter writer) - //{ - // return new ViewContext - // { - // Controller = vc.Controller, - // HttpContext = vc.HttpContext, - // RequestContext = vc.RequestContext, - // RouteData = vc.RouteData, - // TempData = vc.TempData, - // View = vc.View, - // ViewData = vc.ViewData, - // FormContext = vc.FormContext, - // ClientValidationEnabled = vc.ClientValidationEnabled, - // UnobtrusiveJavaScriptEnabled = vc.UnobtrusiveJavaScriptEnabled, - // Writer = writer - // }; - //} - } + // public static ViewContext CloneWithWriter(this ViewContext vc, TextWriter writer) + // { + // return new ViewContext + // { + // Controller = vc.Controller, + // HttpContext = vc.HttpContext, + // RequestContext = vc.RequestContext, + // RouteData = vc.RouteData, + // TempData = vc.TempData, + // View = vc.View, + // ViewData = vc.ViewData, + // FormContext = vc.FormContext, + // ClientValidationEnabled = vc.ClientValidationEnabled, + // UnobtrusiveJavaScriptEnabled = vc.UnobtrusiveJavaScriptEnabled, + // Writer = writer + // }; + // } } diff --git a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs index 2cf6c6f8b9..0ede5a3911 100644 --- a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs @@ -1,154 +1,141 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Extensions -{ - public static class ViewDataExtensions - { - public const string TokenUmbracoPath = "UmbracoPath"; - public const string TokenInstallApiBaseUrl = "InstallApiBaseUrl"; - public const string TokenUmbracoBaseFolder = "UmbracoBaseFolder"; - public const string TokenUmbracoVersion = "UmbracoVersion"; - public const string TokenExternalSignInError = "ExternalSignInError"; - public const string TokenPasswordResetCode = "PasswordResetCode"; - public const string TokenTwoFactorRequired = "TwoFactorRequired"; +namespace Umbraco.Extensions; - public static bool FromTempData(this ViewDataDictionary viewData, ITempDataDictionary tempData, string token) +public static class ViewDataExtensions +{ + public const string TokenUmbracoPath = "UmbracoPath"; + public const string TokenInstallApiBaseUrl = "InstallApiBaseUrl"; + public const string TokenUmbracoBaseFolder = "UmbracoBaseFolder"; + public const string TokenUmbracoVersion = "UmbracoVersion"; + public const string TokenExternalSignInError = "ExternalSignInError"; + public const string TokenPasswordResetCode = "PasswordResetCode"; + public const string TokenTwoFactorRequired = "TwoFactorRequired"; + + public static bool FromTempData(this ViewDataDictionary viewData, ITempDataDictionary tempData, string token) + { + if (tempData[token] == null) { - if (tempData[token] == null) return false; - viewData[token] = tempData[token]; + return false; + } + + viewData[token] = tempData[token]; + return true; + } + + /// + /// Copies data from a request cookie to view data and then clears the cookie in the response + /// + /// + /// + /// + /// This is similar to TempData but in some cases we cannot use TempData which relies on the temp data provider and + /// session. + /// The cookie value can either be a simple string value + /// + /// + public static bool FromBase64CookieData( + this ViewDataDictionary viewData, + HttpContext? httpContext, + string cookieName, + IJsonSerializer serializer) + { + var hasCookie = httpContext?.Request.Cookies.ContainsKey(cookieName) ?? false; + if (!hasCookie) + { + return false; + } + + // get the cookie value + if (httpContext is null || !httpContext.Request.Cookies.TryGetValue(cookieName, out var cookieVal)) + { + return false; + } + + // ensure the cookie is expired (must be done after reading the value) + httpContext.Response.Cookies.Delete(cookieName); + + if (cookieVal.IsNullOrWhiteSpace()) + { + return false; + } + + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(WebUtility.UrlDecode(cookieVal)!)); + + // deserialize to T and store in viewdata + viewData[cookieName] = serializer.Deserialize(decoded); return true; } - - /// - /// Copies data from a request cookie to view data and then clears the cookie in the response - /// - /// - /// - /// - /// - /// - /// - /// This is similar to TempData but in some cases we cannot use TempData which relies on the temp data provider and session. - /// The cookie value can either be a simple string value - /// - /// - public static bool FromBase64CookieData(this ViewDataDictionary viewData, HttpContext? httpContext, string cookieName, IJsonSerializer serializer) + catch (Exception) { - var hasCookie = httpContext?.Request.Cookies.ContainsKey(cookieName) ?? false; - if (!hasCookie) return false; - - // get the cookie value - if (httpContext is null || !httpContext.Request.Cookies.TryGetValue(cookieName, out var cookieVal)) - { - return false; - } - - // ensure the cookie is expired (must be done after reading the value) - httpContext.Response.Cookies.Delete(cookieName); - - if (cookieVal.IsNullOrWhiteSpace()) - return false; - - try - { - var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookieVal)!)); - // deserialize to T and store in viewdata - viewData[cookieName] = serializer.Deserialize(decoded); - return true; - } - catch (Exception) - { - return false; - } - } - - public static string? GetUmbracoPath(this ViewDataDictionary viewData) - { - return (string?)viewData[TokenUmbracoPath]; - } - - public static void SetUmbracoPath(this ViewDataDictionary viewData, string value) - { - viewData[TokenUmbracoPath] = value; - } - - public static string? GetInstallApiBaseUrl(this ViewDataDictionary viewData) - { - return (string?)viewData[TokenInstallApiBaseUrl]; - } - - public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string? value) - { - viewData[TokenInstallApiBaseUrl] = value; - } - - public static string? GetUmbracoBaseFolder(this ViewDataDictionary viewData) - { - return (string?)viewData[TokenUmbracoBaseFolder]; - } - - public static void SetUmbracoBaseFolder(this ViewDataDictionary viewData, string value) - { - viewData[TokenUmbracoBaseFolder] = value; - } - public static void SetUmbracoVersion(this ViewDataDictionary viewData, SemVersion version) - { - viewData[TokenUmbracoVersion] = version; - } - - public static SemVersion? GetUmbracoVersion(this ViewDataDictionary viewData) - { - return (SemVersion?) viewData[TokenUmbracoVersion]; - } - - /// - /// Used by the back office login screen to get any registered external login provider errors - /// - /// - /// - public static BackOfficeExternalLoginProviderErrors? GetExternalSignInProviderErrors(this ViewDataDictionary viewData) - { - return (BackOfficeExternalLoginProviderErrors?)viewData[TokenExternalSignInError]; - } - - /// - /// Used by the back office controller to register any external login provider errors - /// - /// - /// - public static void SetExternalSignInProviderErrors(this ViewDataDictionary viewData, BackOfficeExternalLoginProviderErrors errors) - { - viewData[TokenExternalSignInError] = errors; - } - - public static string? GetPasswordResetCode(this ViewDataDictionary viewData) - { - return (string?)viewData[TokenPasswordResetCode]; - } - - public static void SetPasswordResetCode(this ViewDataDictionary viewData, string value) - { - viewData[TokenPasswordResetCode] = value; - } - - public static void SetTwoFactorProviderNames(this ViewDataDictionary viewData, IEnumerable providerNames) - { - viewData[TokenTwoFactorRequired] = providerNames; - } - - public static bool TryGetTwoFactorProviderNames(this ViewDataDictionary viewData, [MaybeNullWhen(false)] out IEnumerable providerNames) - { - providerNames = viewData[TokenTwoFactorRequired] as IEnumerable; - return providerNames is not null; + return false; } } + + public static string? GetUmbracoPath(this ViewDataDictionary viewData) => (string?)viewData[TokenUmbracoPath]; + + public static void SetUmbracoPath(this ViewDataDictionary viewData, string value) => + viewData[TokenUmbracoPath] = value; + + public static string? GetInstallApiBaseUrl(this ViewDataDictionary viewData) => + (string?)viewData[TokenInstallApiBaseUrl]; + + public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string? value) => + viewData[TokenInstallApiBaseUrl] = value; + + public static string? GetUmbracoBaseFolder(this ViewDataDictionary viewData) => + (string?)viewData[TokenUmbracoBaseFolder]; + + public static void SetUmbracoBaseFolder(this ViewDataDictionary viewData, string value) => + viewData[TokenUmbracoBaseFolder] = value; + + public static void SetUmbracoVersion(this ViewDataDictionary viewData, SemVersion version) => + viewData[TokenUmbracoVersion] = version; + + public static SemVersion? GetUmbracoVersion(this ViewDataDictionary viewData) => + (SemVersion?)viewData[TokenUmbracoVersion]; + + /// + /// Used by the back office login screen to get any registered external login provider errors + /// + /// + /// + public static BackOfficeExternalLoginProviderErrors? + GetExternalSignInProviderErrors(this ViewDataDictionary viewData) => + (BackOfficeExternalLoginProviderErrors?)viewData[TokenExternalSignInError]; + + /// + /// Used by the back office controller to register any external login provider errors + /// + /// + /// + public static void SetExternalSignInProviderErrors( + this ViewDataDictionary viewData, + BackOfficeExternalLoginProviderErrors errors) => viewData[TokenExternalSignInError] = errors; + + public static string? GetPasswordResetCode(this ViewDataDictionary viewData) => + (string?)viewData[TokenPasswordResetCode]; + + public static void SetPasswordResetCode(this ViewDataDictionary viewData, string value) => + viewData[TokenPasswordResetCode] = value; + + public static void SetTwoFactorProviderNames(this ViewDataDictionary viewData, IEnumerable providerNames) => + viewData[TokenTwoFactorRequired] = providerNames; + + public static bool TryGetTwoFactorProviderNames( + this ViewDataDictionary viewData, + [MaybeNullWhen(false)] out IEnumerable providerNames) + { + providerNames = viewData[TokenTwoFactorRequired] as IEnumerable; + return providerNames is not null; + } } diff --git a/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs b/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs index fce5cf92ed..c7bb658ef0 100644 --- a/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs @@ -1,45 +1,44 @@ -using System; -using System.IO; using Microsoft.AspNetCore.Hosting; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Contains extension methods for the interface. +/// +public static class WebHostEnvironmentExtensions { /// - /// Contains extension methods for the interface. + /// Maps a virtual path to a physical path to the application's web root /// - public static class WebHostEnvironmentExtensions + /// + /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the + /// content root are the same, however + /// in netcore the web root is /wwwroot therefore this will Map to a physical path within wwwroot. + /// + public static string MapPathWebRoot(this IWebHostEnvironment webHostEnvironment, string path) { - /// - /// Maps a virtual path to a physical path to the application's web root - /// - /// - /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however - /// in netcore the web root is /wwwroot therefore this will Map to a physical path within wwwroot. - /// - public static string MapPathWebRoot(this IWebHostEnvironment webHostEnvironment, string path) + var root = webHostEnvironment.WebRootPath; + + // Create if missing + if (string.IsNullOrWhiteSpace(root)) { - var root = webHostEnvironment.WebRootPath; - - //Create if missing - if (string.IsNullOrWhiteSpace(root)) - { - root = webHostEnvironment.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); - } - - var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); - - // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX - // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, - // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not - // absolute in the file system. This error will help us find and fix improper uses, and should be removed once - // all those uses have been found and fixed - if (newPath.StartsWith(root)) - { - throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPathWebRoot"); - } - - return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); + root = webHostEnvironment.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); } + + var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + + // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX + // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, + // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not + // absolute in the file system. This error will help us find and fix improper uses, and should be removed once + // all those uses have been found and fixed + if (newPath.StartsWith(root)) + { + throw new ArgumentException( + "The path appears to already be fully qualified. Please remove the call to MapPathWebRoot"); + } + + return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); } } diff --git a/src/Umbraco.Web.Common/Filters/AllowHttpJsonConfigrationAttribute.cs b/src/Umbraco.Web.Common/Filters/AllowHttpJsonConfigrationAttribute.cs index 31fddc65f1..1809267edd 100644 --- a/src/Umbraco.Web.Common/Filters/AllowHttpJsonConfigrationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AllowHttpJsonConfigrationAttribute.cs @@ -1,41 +1,29 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using Umbraco.Cms.Web.Common.Formatters; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +public class AllowHttpJsonConfigrationAttribute : TypeFilterAttribute { - public class AllowHttpJsonConfigrationAttribute : TypeFilterAttribute + /// + /// This filter overwrites AngularJsonOnlyConfigurationAttribute and get the api back to its defualt behavior + /// + public AllowHttpJsonConfigrationAttribute() + : base(typeof(AllowJsonXHRConfigrationFilter)) => + Order = 2; // this value must be more than the AngularJsonOnlyConfigurationAttribute on order to overwrtie it + + private class AllowJsonXHRConfigrationFilter : IResultFilter { - /// - /// This filter overwrites AngularJsonOnlyConfigurationAttribute and get the api back to its defualt behavior - /// - public AllowHttpJsonConfigrationAttribute() : base(typeof(AllowJsonXHRConfigrationFilter)) + public void OnResultExecuted(ResultExecutedContext context) { - Order = 2; // this value must be more than the AngularJsonOnlyConfigurationAttribute on order to overwrtie it } - private class AllowJsonXHRConfigrationFilter : IResultFilter + public void OnResultExecuting(ResultExecutingContext context) { - public void OnResultExecuted(ResultExecutedContext context) + if (context.Result is ObjectResult objectResult) { - } - - public void OnResultExecuting(ResultExecutingContext context) - { - if (context.Result is ObjectResult objectResult) - { - objectResult.Formatters.RemoveType(); - } + objectResult.Formatters.RemoveType(); } } } diff --git a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs index a12dfa3080..ee73649bb2 100644 --- a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; @@ -7,49 +7,47 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using Umbraco.Cms.Web.Common.Formatters; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the +/// angular json vulnerability prevention. +/// +public class AngularJsonOnlyConfigurationAttribute : TypeFilterAttribute { - /// - /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. - /// - public class AngularJsonOnlyConfigurationAttribute : TypeFilterAttribute + public AngularJsonOnlyConfigurationAttribute() + : base(typeof(AngularJsonOnlyConfigurationFilter)) => + Order = 1; // Must be low, to be overridden by other custom formatters, but higher then all framework stuff. + + private class AngularJsonOnlyConfigurationFilter : IResultFilter { - public AngularJsonOnlyConfigurationAttribute() : base(typeof(AngularJsonOnlyConfigurationFilter)) + private readonly ArrayPool _arrayPool; + private readonly MvcOptions _options; + + public AngularJsonOnlyConfigurationFilter(ArrayPool arrayPool, IOptionsSnapshot options) { - Order = 1; // Must be low, to be overridden by other custom formatters, but higher then all framework stuff. + _arrayPool = arrayPool; + _options = options.Value; } - private class AngularJsonOnlyConfigurationFilter : IResultFilter + public void OnResultExecuted(ResultExecutedContext context) { - private readonly ArrayPool _arrayPool; - private MvcOptions _options; + } - public AngularJsonOnlyConfigurationFilter(ArrayPool arrayPool, IOptionsSnapshot options) + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) { - _arrayPool = arrayPool; - _options = options.Value; - } - - public void OnResultExecuted(ResultExecutedContext context) - { - } - - public void OnResultExecuting(ResultExecutingContext context) - { - if (context.Result is ObjectResult objectResult) + var serializerSettings = new JsonSerializerSettings { - var serializerSettings = new JsonSerializerSettings() - { - ContractResolver = new DefaultContractResolver(), - Converters = {new VersionConverter()} - }; + ContractResolver = new DefaultContractResolver(), + Converters = { new VersionConverter() }, + }; - objectResult.Formatters.Clear(); - objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); - } + objectResult.Formatters.Clear(); + objectResult.Formatters.Add( + new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); } } } - - } diff --git a/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs b/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs index 58c934b311..583cd7068b 100644 --- a/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs +++ b/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs @@ -1,37 +1,34 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Globalization; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Applied to all Umbraco controllers to ensure the thread culture is set to the culture assigned to the back office +/// identity +/// +public class BackOfficeCultureFilter : IActionFilter { - /// - /// Applied to all Umbraco controllers to ensure the thread culture is set to the culture assigned to the back office identity - /// - public class BackOfficeCultureFilter : IActionFilter + public void OnActionExecuted(ActionExecutedContext context) { - public void OnActionExecuted(ActionExecutedContext context) - { + } - } - - public void OnActionExecuting(ActionExecutingContext context) + public void OnActionExecuting(ActionExecutingContext context) + { + CultureInfo? culture = context.HttpContext.User.Identity?.GetCulture(); + if (culture != null) { - var culture = context.HttpContext.User.Identity?.GetCulture(); - if (culture != null) - { - SetCurrentThreadCulture(culture); - } - } - - private static void SetCurrentThreadCulture(CultureInfo culture) - { - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = culture; + SetCurrentThreadCulture(culture); } } - + private static void SetCurrentThreadCulture(CultureInfo culture) + { + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } } diff --git a/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs index 8d7b5c6284..803595fa63 100644 --- a/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs @@ -1,35 +1,30 @@ -using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Net.Http.Headers; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Ensures that the request is not cached by the browser +/// +public class DisableBrowserCacheAttribute : ActionFilterAttribute { - /// - /// Ensures that the request is not cached by the browser - /// - public class DisableBrowserCacheAttribute : ActionFilterAttribute + public override void OnResultExecuting(ResultExecutingContext context) { - public override void OnResultExecuting(ResultExecutingContext context) + base.OnResultExecuting(context); + + HttpResponse httpResponse = context.HttpContext.Response; + + if (httpResponse.StatusCode != 200) { - base.OnResultExecuting(context); - - var httpResponse = context.HttpContext.Response; - - if (httpResponse.StatusCode != 200) return; - - httpResponse.GetTypedHeaders().CacheControl = - new CacheControlHeaderValue() - { - NoCache = true, - MaxAge = TimeSpan.Zero, - MustRevalidate = true, - NoStore = true - }; - - httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 - httpResponse.Headers[HeaderNames.Pragma] = "no-cache"; - httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); + return; } + + httpResponse.GetTypedHeaders().CacheControl = + new CacheControlHeaderValue { NoCache = true, MaxAge = TimeSpan.Zero, MustRevalidate = true, NoStore = true }; + + httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 + httpResponse.Headers[HeaderNames.Pragma] = "no-cache"; + httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); } } diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs index ac065b5a92..b6ca40efc2 100644 --- a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -1,5 +1,3 @@ -using System.IO; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Rendering; @@ -8,89 +6,87 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Umbraco.Cms.Web.Common.Constants; using Umbraco.Cms.Web.Common.Controllers; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// This is a special filter which is required for the RTE to be able to render Partial View Macros that +/// contain forms when the RTE value is resolved outside of an MVC view being rendered +/// +/// +/// The entire way that we support partial view macros that contain forms isn't really great, these forms +/// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action +/// so the form can show errors, viewdata, etc... +/// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases +/// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly +/// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is +/// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. +/// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that +/// this DataToken exists before the action executes in case the developer resolves an RTE value that contains +/// a partial view macro form. +/// +public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute { /// - /// This is a special filter which is required for the RTE to be able to render Partial View Macros that - /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still + /// render any PartialViewMacro with a form and maintain ModelState /// - /// - /// The entire way that we support partial view macros that contain forms isn't really great, these forms - /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action - /// so the form can show errors, viewdata, etc... - /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases - /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly - /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is - /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. - /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that - /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains - /// a partial view macro form. - /// - public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + public override void OnActionExecuting(ActionExecutingContext context) { - - /// - /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, - /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still - /// render any PartialViewMacro with a form and maintain ModelState - /// - public override void OnActionExecuting(ActionExecutingContext context) + if (!(context.Controller is Controller controller)) { - if (!(context.Controller is Controller controller)) - { - return; - } - - // ignore anything that is not IRenderController - if (!(controller is IRenderController)) - { - return; - } - - SetViewContext(context, controller); + return; } - /// - /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, - /// this ensures that any custom ModelState that may have been added in the RenderController itself is - /// passed onwards in case it is required when rendering a PartialViewMacro with a form - /// - /// The filter context. - public override void OnResultExecuting(ResultExecutingContext context) + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) { - if (!(context.Controller is Controller controller)) - { - return; - } - - // ignore anything that is not IRenderController - if (!(controller is IRenderController)) - { - return; - } - - SetViewContext(context, controller); + return; } - private void SetViewContext(ActionContext context, Controller controller) - { - var viewCtx = new ViewContext( - context, - new DummyView(), - controller.ViewData, - controller.TempData, - new StringWriter(), - new HtmlHelperOptions()); + SetViewContext(context, controller); + } - // set the special data token - context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Controller is Controller controller)) + { + return; } - private class DummyView : IView + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) { - public Task RenderAsync(ViewContext context) => Task.CompletedTask; - - public string Path { get; } = null!; + return; } + + SetViewContext(context, controller); + } + + private void SetViewContext(ActionContext context, Controller controller) + { + var viewCtx = new ViewContext( + context, + new DummyView(), + controller.ViewData, + controller.TempData, + new StringWriter(), + new HtmlHelperOptions()); + + // set the special data token + context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public string Path { get; } = null!; + + public Task RenderAsync(ViewContext context) => Task.CompletedTask; } } diff --git a/src/Umbraco.Web.Common/Filters/ExceptionViewModel.cs b/src/Umbraco.Web.Common/Filters/ExceptionViewModel.cs index f53fc80d78..e245e086da 100644 --- a/src/Umbraco.Web.Common/Filters/ExceptionViewModel.cs +++ b/src/Umbraco.Web.Common/Filters/ExceptionViewModel.cs @@ -1,11 +1,10 @@ -using System; +namespace Umbraco.Cms.Web.Common.Filters; -namespace Umbraco.Cms.Web.Common.Filters +public class ExceptionViewModel { - public class ExceptionViewModel - { - public string? ExceptionMessage { get; set; } - public Type? ExceptionType { get; set; } - public string? StackTrace { get; set; } - } + public string? ExceptionMessage { get; set; } + + public Type? ExceptionType { get; set; } + + public string? StackTrace { get; set; } } diff --git a/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs index 98d818ba08..247a2a09ba 100644 --- a/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs @@ -6,53 +6,47 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Umbraco.Cms.Web.Common.Formatters; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. +/// +public sealed class JsonDateTimeFormatAttribute : TypeFilterAttribute { /// - /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// Initializes a new instance of the class. /// - public sealed class JsonDateTimeFormatAttribute : TypeFilterAttribute + public JsonDateTimeFormatAttribute() + : base(typeof(JsonDateTimeFormatFilter)) => + Order = 2; // must be higher than AngularJsonOnlyConfigurationAttribute.Order + + private class JsonDateTimeFormatFilter : IResultFilter { - /// - /// Initializes a new instance of the class. - /// - public JsonDateTimeFormatAttribute() - : base(typeof(JsonDateTimeFormatFilter)) => - Order = 2; // must be higher than AngularJsonOnlyConfigurationAttribute.Order + private readonly ArrayPool _arrayPool; + private readonly string _format = "yyyy-MM-dd HH:mm:ss"; + private readonly MvcOptions _options; - private class JsonDateTimeFormatFilter : IResultFilter + public JsonDateTimeFormatFilter(ArrayPool arrayPool, IOptionsSnapshot options) { - private readonly string _format = "yyyy-MM-dd HH:mm:ss"; + _arrayPool = arrayPool; + _options = options.Value; + } - private readonly ArrayPool _arrayPool; - private readonly MvcOptions _options; + public void OnResultExecuted(ResultExecutedContext context) + { + } - public JsonDateTimeFormatFilter(ArrayPool arrayPool, IOptionsSnapshot options) + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) { - _arrayPool = arrayPool; - _options = options.Value; - } - - public void OnResultExecuted(ResultExecutedContext context) - { - } - - public void OnResultExecuting(ResultExecutingContext context) - { - if (context.Result is ObjectResult objectResult) - { - var serializerSettings = new JsonSerializerSettings(); - serializerSettings.Converters.Add( - new IsoDateTimeConverter - { - DateTimeFormat = _format - }); - objectResult.Formatters.Clear(); - objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); - } + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.Converters.Add( + new IsoDateTimeConverter { DateTimeFormat = _format }); + objectResult.Formatters.Clear(); + objectResult.Formatters.Add( + new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); } } } - - } diff --git a/src/Umbraco.Web.Common/Filters/JsonExceptionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/JsonExceptionFilterAttribute.cs index 098192365d..90135a92c3 100644 --- a/src/Umbraco.Web.Common/Filters/JsonExceptionFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/JsonExceptionFilterAttribute.cs @@ -1,4 +1,3 @@ -using System; using System.Net; using System.Net.Mime; using Microsoft.AspNetCore.Mvc; @@ -6,58 +5,50 @@ using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +public class JsonExceptionFilterAttribute : TypeFilterAttribute { - public class JsonExceptionFilterAttribute : TypeFilterAttribute + public JsonExceptionFilterAttribute() + : base(typeof(JsonExceptionFilter)) { - public JsonExceptionFilterAttribute() : base(typeof(JsonExceptionFilter)) + } + + private class JsonExceptionFilter : IExceptionFilter + { + private readonly IHostingEnvironment _hostingEnvironment; + + public JsonExceptionFilter(IHostingEnvironment hostingEnvironment) => _hostingEnvironment = hostingEnvironment; + + public void OnException(ExceptionContext filterContext) { + if (!filterContext.ExceptionHandled) + { + filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + + filterContext.Result = new ContentResult + { + StatusCode = (int)HttpStatusCode.InternalServerError, + ContentType = MediaTypeNames.Application.Json, + Content = JsonConvert.SerializeObject( + GetModel(filterContext.Exception), + new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }), + }; + filterContext.ExceptionHandled = true; + } } - private class JsonExceptionFilter : IExceptionFilter + private object GetModel(Exception ex) { - private readonly IHostingEnvironment _hostingEnvironment; + var error = new ExceptionViewModel { ExceptionMessage = ex.Message }; - public JsonExceptionFilter(IHostingEnvironment hostingEnvironment) + if (_hostingEnvironment.IsDebugMode) { - _hostingEnvironment = hostingEnvironment; + error.ExceptionType = ex.GetType(); + error.StackTrace = ex.StackTrace; } - public void OnException(ExceptionContext filterContext) - { - if (filterContext.Exception != null && !filterContext.ExceptionHandled) - { - filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.InternalServerError; - - filterContext.Result = new ContentResult - { - StatusCode = (int)HttpStatusCode.InternalServerError, - ContentType = MediaTypeNames.Application.Json, - Content = JsonConvert.SerializeObject(GetModel(filterContext.Exception), - new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }) - }; - filterContext.ExceptionHandled = true; - } - } - - private object GetModel(Exception ex) - { - var error = new ExceptionViewModel - { - ExceptionMessage = ex.Message - }; - - if (_hostingEnvironment.IsDebugMode) - { - error.ExceptionType = ex.GetType(); - error.StackTrace = ex.StackTrace; - } - - return error; - } + return error; } } } diff --git a/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs b/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs index 1ef244dbe3..0129874deb 100644 --- a/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs @@ -1,4 +1,3 @@ -using System; using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http.Extensions; @@ -11,79 +10,91 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// An exception filter checking if we get a or +/// with the same model. +/// In which case it returns a redirect to the same page after 1 sec if not in debug mode. +/// +/// +/// This is only enabled when using mode +/// +public sealed class ModelBindingExceptionAttribute : TypeFilterAttribute { /// - /// An exception filter checking if we get a or with the same model. - /// In which case it returns a redirect to the same page after 1 sec if not in debug mode. + /// Initializes a new instance of the class. /// - /// - /// This is only enabled when using mode - /// - public sealed class ModelBindingExceptionAttribute : TypeFilterAttribute + public ModelBindingExceptionAttribute() + : base(typeof(ModelBindingExceptionFilter)) { - /// - /// Initializes a new instance of the class. - /// - public ModelBindingExceptionAttribute() - : base(typeof(ModelBindingExceptionFilter)) + } + + private class ModelBindingExceptionFilter : IExceptionFilter + { + private static readonly Regex GetPublishedModelsTypesRegex = + new("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled); + + private readonly ExceptionFilterSettings _exceptionFilterSettings; + private readonly IPublishedModelFactory _publishedModelFactory; + + public ModelBindingExceptionFilter( + IOptionsSnapshot exceptionFilterSettings, + IPublishedModelFactory publishedModelFactory) { + _exceptionFilterSettings = exceptionFilterSettings.Value; + _publishedModelFactory = + publishedModelFactory ?? throw new ArgumentNullException(nameof(publishedModelFactory)); } - private class ModelBindingExceptionFilter : IExceptionFilter + public void OnException(ExceptionContext filterContext) { - private static readonly Regex s_getPublishedModelsTypesRegex = new Regex("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled); - - private readonly ExceptionFilterSettings _exceptionFilterSettings; - private readonly IPublishedModelFactory _publishedModelFactory; - - public ModelBindingExceptionFilter(IOptionsSnapshot exceptionFilterSettings, IPublishedModelFactory publishedModelFactory) + var disabled = _exceptionFilterSettings.Disabled; + if (_publishedModelFactory.IsLiveFactoryEnabled() + && !disabled + && !filterContext.ExceptionHandled + && (filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException) + && IsMessageAboutTheSameModelType(filterContext.Exception.Message)) { - _exceptionFilterSettings = exceptionFilterSettings.Value; - _publishedModelFactory = publishedModelFactory ?? throw new ArgumentNullException(nameof(publishedModelFactory)); + filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1"); + filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.GetEncodedUrl(), false); + + filterContext.ExceptionHandled = true; + } + } + + /// + /// Returns true if the message is about two models with the same name. + /// + /// + /// Message could be something like: + /// + /// InvalidCastException: + /// [A]Umbraco.Web.PublishedModels.Home cannot be cast to [B]Umbraco.Web.PublishedModels.Home. Type A originates + /// from 'App_Web_all.generated.cs.8f9494c4.rtdigm_z, Version=0.0.0.3, Culture=neutral, PublicKeyToken=null' in the + /// context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET + /// Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rtdigm_z.dll'. Type B originates from + /// 'App_Web_all.generated.cs.8f9494c4.rbyqlplu, Version=0.0.0.5, Culture=neutral, PublicKeyToken=null' in the + /// context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET + /// Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rbyqlplu.dll'. + /// + /// + /// ModelBindingException: + /// Cannot bind source content type Umbraco.Web.PublishedModels.Home to model type + /// Umbraco.Web.PublishedModels.Home. Both view and content models are generated models, with different versions. + /// The application is in an unstable state and is going to be restarted. The application is restarting now. + /// + /// + private bool IsMessageAboutTheSameModelType(string exceptionMessage) + { + MatchCollection matches = GetPublishedModelsTypesRegex.Matches(exceptionMessage); + + if (matches.Count >= 2) + { + return string.Equals(matches[0].Value, matches[1].Value, StringComparison.InvariantCulture); } - public void OnException(ExceptionContext filterContext) - { - var disabled = _exceptionFilterSettings?.Disabled ?? false; - if (_publishedModelFactory.IsLiveFactoryEnabled() - && !disabled - && !filterContext.ExceptionHandled - && (filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException) - && IsMessageAboutTheSameModelType(filterContext.Exception.Message)) - { - filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1"); - filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.GetEncodedUrl(), false); - - filterContext.ExceptionHandled = true; - } - } - - /// - /// Returns true if the message is about two models with the same name. - /// - /// - /// Message could be something like: - /// - /// InvalidCastException: - /// [A]Umbraco.Web.PublishedModels.Home cannot be cast to [B]Umbraco.Web.PublishedModels.Home. Type A originates from 'App_Web_all.generated.cs.8f9494c4.rtdigm_z, Version=0.0.0.3, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rtdigm_z.dll'. Type B originates from 'App_Web_all.generated.cs.8f9494c4.rbyqlplu, Version=0.0.0.5, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rbyqlplu.dll'. - /// - /// - /// ModelBindingException: - /// Cannot bind source content type Umbraco.Web.PublishedModels.Home to model type Umbraco.Web.PublishedModels.Home. Both view and content models are generated models, with different versions. The application is in an unstable state and is going to be restarted. The application is restarting now. - /// - /// - private bool IsMessageAboutTheSameModelType(string exceptionMessage) - { - MatchCollection matches = s_getPublishedModelsTypesRegex.Matches(exceptionMessage); - - if (matches.Count >= 2) - { - return string.Equals(matches[0].Value, matches[1].Value, StringComparison.InvariantCulture); - } - - return false; - } + return false; } } } diff --git a/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs b/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs index 16613b4289..0ecb3929d5 100644 --- a/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs @@ -1,82 +1,76 @@ -using System; using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Web.Common.Formatters; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +public class OutgoingNoHyphenGuidFormatAttribute : TypeFilterAttribute { - public class OutgoingNoHyphenGuidFormatAttribute : TypeFilterAttribute + public OutgoingNoHyphenGuidFormatAttribute() + : base(typeof(OutgoingNoHyphenGuidFormatFilter)) => + Order = 2; // must be higher than AngularJsonOnlyConfigurationAttribute.Order + + private class OutgoingNoHyphenGuidFormatFilter : IResultFilter { - public OutgoingNoHyphenGuidFormatAttribute() : base(typeof(OutgoingNoHyphenGuidFormatFilter)) + private readonly ArrayPool _arrayPool; + private readonly MvcOptions _options; + + public OutgoingNoHyphenGuidFormatFilter(ArrayPool arrayPool, IOptionsSnapshot options) { - Order = 2; //must be higher than AngularJsonOnlyConfigurationAttribute.Order + _arrayPool = arrayPool; + _options = options.Value; } - private class OutgoingNoHyphenGuidFormatFilter : IResultFilter + public void OnResultExecuted(ResultExecutedContext context) { - private readonly ArrayPool _arrayPool; - private readonly MvcOptions _options; + } - public OutgoingNoHyphenGuidFormatFilter(ArrayPool arrayPool, IOptionsSnapshot options) + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) { - _arrayPool = arrayPool; - _options = options.Value; - } - public void OnResultExecuted(ResultExecutedContext context) - { - } + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.Converters.Add(new GuidNoHyphenConverter()); - public void OnResultExecuting(ResultExecutingContext context) + objectResult.Formatters.Clear(); + objectResult.Formatters.Add( + new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); + } + } + + /// + /// A custom converter for GUID's to format without hyphens + /// + private class GuidNoHyphenConverter : JsonConverter + { + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - if (context.Result is ObjectResult objectResult) + switch (reader.TokenType) { - var serializerSettings = new JsonSerializerSettings(); - serializerSettings.Converters.Add(new GuidNoHyphenConverter()); + case JsonToken.Null: + return Guid.Empty; + case JsonToken.String: + Attempt guidAttempt = reader.Value.TryConvertTo(); + if (guidAttempt.Success) + { + return guidAttempt.Result; + } - objectResult.Formatters.Clear(); - objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options)); + throw new FormatException("Could not convert " + reader.Value + " to a GUID"); + default: + throw new ArgumentException("Invalid token type"); } } - /// - /// A custom converter for GUID's to format without hyphens - /// - private class GuidNoHyphenConverter : JsonConverter - { - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - switch (reader.TokenType) - { - case JsonToken.Null: - return Guid.Empty; - case JsonToken.String: - var guidAttempt = reader.Value.TryConvertTo(); - if (guidAttempt.Success) - { - return guidAttempt.Result; - } - throw new FormatException("Could not convert " + reader.Value + " to a GUID"); - default: - throw new ArgumentException("Invalid token type"); - } - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => + writer.WriteValue(Guid.Empty.Equals(value) ? Guid.Empty.ToString("N") : ((Guid?)value)?.ToString("N")); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteValue(Guid.Empty.Equals(value) ? Guid.Empty.ToString("N") : ((Guid?)value)?.ToString("N")); - } - - public override bool CanConvert(Type objectType) - { - return typeof(Guid) == objectType; - } - } + public override bool CanConvert(Type objectType) => typeof(Guid) == objectType; } } - - } diff --git a/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs index 3ad49c1732..57c33b9684 100644 --- a/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs @@ -1,40 +1,38 @@ -using System.Net; +using System.Net; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Forces the response to have a specific http status code +/// +public class StatusCodeResultAttribute : ActionFilterAttribute { - /// - /// Forces the response to have a specific http status code - /// - public class StatusCodeResultAttribute : ActionFilterAttribute + private readonly HttpStatusCode _statusCode; + + public StatusCodeResultAttribute(HttpStatusCode statusCode) => _statusCode = statusCode; + + public override void OnActionExecuted(ActionExecutedContext context) { - private readonly HttpStatusCode _statusCode; + base.OnActionExecuted(context); - public StatusCodeResultAttribute(HttpStatusCode statusCode) + HttpContext httpContext = context.HttpContext; + + httpContext.Response.StatusCode = (int)_statusCode; + + var disableIisCustomErrors = httpContext.RequestServices.GetService>()?.Value + .TrySkipIisCustomErrors ?? false; + IStatusCodePagesFeature? statusCodePagesFeature = httpContext.Features.Get(); + + if (statusCodePagesFeature != null) { - _statusCode = statusCode; - } - - public override void OnActionExecuted(ActionExecutedContext context) - { - base.OnActionExecuted(context); - - var httpContext = context.HttpContext; - - httpContext.Response.StatusCode = (int)_statusCode; - - var disableIisCustomErrors = httpContext.RequestServices.GetService>()?.Value.TrySkipIisCustomErrors ?? false; - var statusCodePagesFeature = httpContext.Features.Get(); - - if (statusCodePagesFeature != null) - { - // if IIS Custom Errors are disabled, we won't enable the Status Code Pages - statusCodePagesFeature.Enabled = !disableIisCustomErrors; - } + // if IIS Custom Errors are disabled, we won't enable the Status Code Pages + statusCodePagesFeature.Enabled = !disableIisCustomErrors; } } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs index 603a9c421b..0940d6abde 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs @@ -1,20 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Ensures authorization is successful for a website user (member). +/// +public class UmbracoMemberAuthorizeAttribute : TypeFilterAttribute { - /// - /// Ensures authorization is successful for a website user (member). - /// - public class UmbracoMemberAuthorizeAttribute : TypeFilterAttribute + public UmbracoMemberAuthorizeAttribute() + : this(string.Empty, string.Empty, string.Empty) { - public UmbracoMemberAuthorizeAttribute() : this(string.Empty, string.Empty, string.Empty) - { - } - - public UmbracoMemberAuthorizeAttribute(string allowType, string allowGroup, string allowMembers) : base(typeof(UmbracoMemberAuthorizeFilter)) - { - Arguments = new object[] { allowType, allowGroup, allowMembers}; - } - } + + public UmbracoMemberAuthorizeAttribute(string allowType, string allowGroup, string allowMembers) + : base(typeof(UmbracoMemberAuthorizeFilter)) => Arguments = new object[] { allowType, allowGroup, allowMembers }; } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 136d2f1c8d..351ea6e1bf 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; using System.Globalization; -using System.Threading.Tasks; - using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -11,113 +8,115 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Ensures authorization is successful for a front-end member +/// +public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter { + public UmbracoMemberAuthorizeFilter() + { + AllowType = string.Empty; + AllowGroup = string.Empty; + AllowMembers = string.Empty; + } + + public UmbracoMemberAuthorizeFilter(string allowType, string allowGroup, string allowMembers) + { + AllowType = allowType; + AllowGroup = allowGroup; + AllowMembers = allowMembers; + } /// - /// Ensures authorization is successful for a front-end member + /// Comma delimited list of allowed member types /// - public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter + public string AllowType { get; private set; } + + /// + /// Comma delimited list of allowed member groups + /// + public string AllowGroup { get; private set; } + + /// + /// Comma delimited list of allowed members + /// + public string AllowMembers { get; private set; } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { - public UmbracoMemberAuthorizeFilter() + // Allow Anonymous skips all authorization + if (HasAllowAnonymous(context)) { - AllowType = string.Empty; - AllowGroup = string.Empty; - AllowMembers = string.Empty; + return; } - public UmbracoMemberAuthorizeFilter(string allowType, string allowGroup, string allowMembers) + IMemberManager memberManager = context.HttpContext.RequestServices.GetRequiredService(); + + if (!await IsAuthorizedAsync(memberManager)) { - AllowType = allowType; - AllowGroup = allowGroup; - AllowMembers = allowMembers; + context.HttpContext.SetReasonPhrase( + "Resource restricted: either member is not logged on or is not of a permitted type or group."); + context.Result = new ForbidResult(); } + } - /// - /// Comma delimited list of allowed member types - /// - public string AllowType { get; private set; } - - /// - /// Comma delimited list of allowed member groups - /// - public string AllowGroup { get; private set; } - - /// - /// Comma delimited list of allowed members - /// - public string AllowMembers { get; private set; } - - public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + /// + /// Copied from https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs + /// + private bool HasAllowAnonymous(AuthorizationFilterContext context) + { + IList filters = context.Filters; + for (var i = 0; i < filters.Count; i++) { - // Allow Anonymous skips all authorization - if (HasAllowAnonymous(context)) - { - return; - } - - IMemberManager memberManager = context.HttpContext.RequestServices.GetRequiredService(); - - if (!await IsAuthorizedAsync(memberManager)) - { - context.HttpContext.SetReasonPhrase("Resource restricted: either member is not logged on or is not of a permitted type or group."); - context.Result = new ForbidResult(); - } - } - - /// - /// Copied from https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs - /// - private bool HasAllowAnonymous(AuthorizationFilterContext context) - { - var filters = context.Filters; - for (var i = 0; i < filters.Count; i++) - { - if (filters[i] is IAllowAnonymousFilter) - { - return true; - } - } - - // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that - // were discovered on controllers and actions. To maintain compat with 2.x, - // we'll check for the presence of IAllowAnonymous in endpoint metadata. - var endpoint = context.HttpContext.GetEndpoint(); - if (endpoint?.Metadata?.GetMetadata() != null) + if (filters[i] is IAllowAnonymousFilter) { return true; } - - return false; } - private async Task IsAuthorizedAsync(IMemberManager memberManager) + // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that + // were discovered on controllers and actions. To maintain compat with 2.x, + // we'll check for the presence of IAllowAnonymous in endpoint metadata. + Endpoint? endpoint = context.HttpContext.GetEndpoint(); + if (endpoint?.Metadata?.GetMetadata() != null) { - if (AllowMembers.IsNullOrWhiteSpace()) - { - AllowMembers = string.Empty; - } - - if (AllowGroup.IsNullOrWhiteSpace()) - { - AllowGroup = string.Empty; - } - - if (AllowType.IsNullOrWhiteSpace()) - { - AllowType = string.Empty; - } - - var members = new List(); - foreach (var s in AllowMembers.Split(Core.Constants.CharArrays.Comma)) - { - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) - { - members.Add(id); - } - } - - return await memberManager.IsMemberAuthorizedAsync(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); + return true; } + + return false; + } + + private async Task IsAuthorizedAsync(IMemberManager memberManager) + { + if (AllowMembers.IsNullOrWhiteSpace()) + { + AllowMembers = string.Empty; + } + + if (AllowGroup.IsNullOrWhiteSpace()) + { + AllowGroup = string.Empty; + } + + if (AllowType.IsNullOrWhiteSpace()) + { + AllowType = string.Empty; + } + + var members = new List(); + foreach (var s in AllowMembers.Split(Core.Constants.CharArrays.Comma)) + { + if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) + { + members.Add(id); + } + } + + return await memberManager.IsMemberAuthorizedAsync( + AllowType.Split(Core.Constants.CharArrays.Comma), + AllowGroup.Split(Core.Constants.CharArrays.Comma), + members); } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoUserTimeoutFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoUserTimeoutFilterAttribute.cs index b42962140d..13c62365c2 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoUserTimeoutFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoUserTimeoutFilterAttribute.cs @@ -3,34 +3,39 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// This will check if the user making the request is authenticated and if there's an auth ticket tied to the user +/// we will add a custom header to the response indicating how many seconds are remaining for the +/// user's session. This allows us to keep track of a user's session effectively in the back office. +/// +public class UmbracoUserTimeoutFilterAttribute : TypeFilterAttribute { - /// - /// This will check if the user making the request is authenticated and if there's an auth ticket tied to the user - /// we will add a custom header to the response indicating how many seconds are remaining for the - /// user's session. This allows us to keep track of a user's session effectively in the back office. - /// - public class UmbracoUserTimeoutFilterAttribute : TypeFilterAttribute + public UmbracoUserTimeoutFilterAttribute() + : base(typeof(UmbracoUserTimeoutFilter)) { - public UmbracoUserTimeoutFilterAttribute() : base(typeof(UmbracoUserTimeoutFilter)) + } + + private class UmbracoUserTimeoutFilter : IActionFilter + { + public void OnActionExecuted(ActionExecutedContext context) { + // This can occur if an error has already occurred. + if (context.HttpContext.Response is null) + { + return; + } + + var remainingSeconds = context.HttpContext.User.GetRemainingAuthSeconds(); + context.HttpContext.Response.Headers.Add( + "X-Umb-User-Seconds", + remainingSeconds.ToString(CultureInfo.InvariantCulture)); } - private class UmbracoUserTimeoutFilter : IActionFilter + public void OnActionExecuting(ActionExecutingContext context) { - public void OnActionExecuted(ActionExecutedContext context) - { - //this can occur if an error has already occurred. - if (context.HttpContext.Response is null) return; - - var remainingSeconds = context.HttpContext.User.GetRemainingAuthSeconds(); - context.HttpContext.Response.Headers.Add("X-Umb-User-Seconds", remainingSeconds.ToString(CultureInfo.InvariantCulture)); - } - - public void OnActionExecuting(ActionExecutingContext context) - { - // Noop - } + // Noop } } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs index f4d95e8282..7b1b9960e3 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs @@ -1,83 +1,78 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; -using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters +namespace Umbraco.Cms.Web.Common.Filters; + +/// +/// Used to set the request feature based on the +/// specified (if any) +/// for the custom route. +/// +public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter { - /// - /// Used to set the request feature based on the specified (if any) - /// for the custom route. - /// - public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - /// - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + Endpoint? endpoint = context.HttpContext.GetEndpoint(); + + // Check if there is any delegate in the metadata of the route, this + // will occur when using the ForUmbraco method during routing. + CustomRouteContentFinderDelegate? contentFinder = + endpoint?.Metadata.OfType().FirstOrDefault(); + + if (contentFinder != null) { - Endpoint? endpoint = context.HttpContext.GetEndpoint(); - - // Check if there is any delegate in the metadata of the route, this - // will occur when using the ForUmbraco method during routing. - CustomRouteContentFinderDelegate? contentFinder = endpoint?.Metadata.OfType().FirstOrDefault(); - - if (contentFinder != null) + await SetUmbracoRouteValues(context, contentFinder.FindContent(context)); + } + else + { + // Check if the controller is IVirtualPageController and then use that to FindContent + if (context.Controller is IVirtualPageController ctrl) { - await SetUmbracoRouteValues(context, contentFinder.FindContent(context)); - } - else - { - // Check if the controller is IVirtualPageController and then use that to FindContent - if (context.Controller is IVirtualPageController ctrl) - { - await SetUmbracoRouteValues(context, ctrl.FindContent(context)); - } - } - - // if we've assigned not found, just exit - if (!(context.Result is NotFoundResult)) - { - await next(); + await SetUmbracoRouteValues(context, ctrl.FindContent(context)); } } - private async Task SetUmbracoRouteValues(ActionExecutingContext context, IPublishedContent content) + // if we've assigned not found, just exit + if (!(context.Result is NotFoundResult)) { - if (content != null) - { - UriUtility uriUtility = context.HttpContext.RequestServices.GetRequiredService(); + await next(); + } + } - Uri originalRequestUrl = new Uri(context.HttpContext.Request.GetEncodedUrl()); - Uri cleanedUrl = uriUtility.UriToUmbraco(originalRequestUrl); + private async Task SetUmbracoRouteValues(ActionExecutingContext context, IPublishedContent? content) + { + if (content != null) + { + UriUtility uriUtility = context.HttpContext.RequestServices.GetRequiredService(); - IPublishedRouter router = context.HttpContext.RequestServices.GetRequiredService(); + var originalRequestUrl = new Uri(context.HttpContext.Request.GetEncodedUrl()); + Uri cleanedUrl = uriUtility.UriToUmbraco(originalRequestUrl); - IPublishedRequestBuilder requestBuilder = await router.CreateRequestAsync(cleanedUrl); - requestBuilder.SetPublishedContent(content); - IPublishedRequest publishedRequest = requestBuilder.Build(); + IPublishedRouter router = context.HttpContext.RequestServices.GetRequiredService(); - var routeValues = new UmbracoRouteValues( - publishedRequest, - (ControllerActionDescriptor)context.ActionDescriptor); + IPublishedRequestBuilder requestBuilder = await router.CreateRequestAsync(cleanedUrl); + requestBuilder.SetPublishedContent(content); + IPublishedRequest publishedRequest = requestBuilder.Build(); - context.HttpContext.Features.Set(routeValues); - } - else - { - // if there is no content then it should be a not found - context.Result = new NotFoundResult(); - } + var routeValues = new UmbracoRouteValues( + publishedRequest, + (ControllerActionDescriptor)context.ActionDescriptor); + + context.HttpContext.Features.Set(routeValues); + } + else + { + // if there is no content then it should be a not found + context.Result = new NotFoundResult(); } } } diff --git a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs index 3337f74a4e..791dfc0c12 100644 --- a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -8,68 +7,66 @@ using Umbraco.Cms.Web.Common.Exceptions; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Filters -{ +namespace Umbraco.Cms.Web.Common.Filters; - /// - /// Attribute used to check that the request contains a valid Umbraco form request string. - /// - /// +/// +/// Attribute used to check that the request contains a valid Umbraco form request string. +/// +/// /// Applying this attribute/filter to a or SurfaceController Action will ensure that the Action can only be executed /// when it is routed to from within Umbraco, typically when rendering a form with BeginUmbracoForm. It will mean that the natural MVC route for this Action - /// will fail with a . - /// - public class ValidateUmbracoFormRouteStringAttribute : TypeFilterAttribute +/// will fail with a . +/// +public class ValidateUmbracoFormRouteStringAttribute : TypeFilterAttribute +{ + // TODO: Lets revisit this when we get members done and the front-end working and whether it can moved to an authz policy + public ValidateUmbracoFormRouteStringAttribute() + : base(typeof(ValidateUmbracoFormRouteStringFilter)) => + Arguments = new object[] { }; + + internal class ValidateUmbracoFormRouteStringFilter : IAuthorizationFilter { + private readonly IDataProtectionProvider _dataProtectionProvider; - // TODO: Lets revisit this when we get members done and the front-end working and whether it can moved to an authz policy + public ValidateUmbracoFormRouteStringFilter(IDataProtectionProvider dataProtectionProvider) => + _dataProtectionProvider = dataProtectionProvider; - public ValidateUmbracoFormRouteStringAttribute() : base(typeof(ValidateUmbracoFormRouteStringFilter)) + public void OnAuthorization(AuthorizationFilterContext context) { - Arguments = new object[] { }; + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var ufprt = context.HttpContext.Request.GetUfprt(); + + if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + ValidateRouteString(ufprt, controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName, context.RouteData.DataTokens["area"]?.ToString()); + } } - internal class ValidateUmbracoFormRouteStringFilter: IAuthorizationFilter + public void ValidateRouteString(string? ufprt, string currentController, string currentAction, string? currentArea) { - private readonly IDataProtectionProvider _dataProtectionProvider; - - public ValidateUmbracoFormRouteStringFilter(IDataProtectionProvider dataProtectionProvider) + if (ufprt.IsNullOrWhiteSpace()) { - _dataProtectionProvider = dataProtectionProvider; + throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); } - public void OnAuthorization(AuthorizationFilterContext context) + if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(_dataProtectionProvider, ufprt!, out IDictionary? additionalDataParts)) { - if (context == null) throw new ArgumentNullException(nameof(context)); - - var ufprt = context.HttpContext.Request.GetUfprt(); - - if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - ValidateRouteString(ufprt, controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName, context.RouteData?.DataTokens["area"]?.ToString()); - } - + throw new HttpUmbracoFormRouteStringException( + "The Umbraco form request route string could not be decrypted."); } - public void ValidateRouteString(string? ufprt, string currentController, string currentAction, string? currentArea) + if (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Controller] + .InvariantEquals(currentController) || + !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || + (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && + !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) { - if (ufprt.IsNullOrWhiteSpace()) - { - throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); - } - - if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(_dataProtectionProvider, ufprt!, out var additionalDataParts)) - { - throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted."); - } - - if (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Controller].InvariantEquals(currentController) || - !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || - (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) - { - throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); - } - + throw new HttpUmbracoFormRouteStringException( + "The provided Umbraco form request route string was meant for a different controller and action."); } } } diff --git a/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs index 55063b970c..cea63d516a 100644 --- a/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs +++ b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs @@ -1,44 +1,41 @@ -using System.Buffers; -using System.IO; +using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Web.Common.Formatters +namespace Umbraco.Cms.Web.Common.Formatters; + +/// +/// This will format the JSON output for use with AngularJs's approach to JSON Vulnerability attacks +/// +/// +/// See: http://docs.angularjs.org/api/ng.$http (Security considerations) +/// +public class AngularJsonMediaTypeFormatter : NewtonsoftJsonOutputFormatter { - /// - /// This will format the JSON output for use with AngularJs's approach to JSON Vulnerability attacks - /// - /// - /// See: http://docs.angularjs.org/api/ng.$http (Security considerations) - /// - public class AngularJsonMediaTypeFormatter : NewtonsoftJsonOutputFormatter + public const string XsrfPrefix = ")]}',\n"; + + public AngularJsonMediaTypeFormatter(JsonSerializerSettings serializerSettings, ArrayPool charPool, MvcOptions mvcOptions) + : base(RegisterJsonConverters(serializerSettings), charPool, mvcOptions) { - public const string XsrfPrefix = ")]}',\n"; + } - public AngularJsonMediaTypeFormatter(JsonSerializerSettings serializerSettings, ArrayPool charPool, MvcOptions mvcOptions) - : base(RegisterJsonConverters(serializerSettings), charPool, mvcOptions) - { + protected static JsonSerializerSettings RegisterJsonConverters(JsonSerializerSettings serializerSettings) + { + serializerSettings.Converters.Add(new StringEnumConverter()); + serializerSettings.Converters.Add(new UdiJsonConverter()); - } + return serializerSettings; + } - protected override JsonWriter CreateJsonWriter(TextWriter writer) - { - var jsonWriter = base.CreateJsonWriter(writer); + protected override JsonWriter CreateJsonWriter(TextWriter writer) + { + JsonWriter jsonWriter = base.CreateJsonWriter(writer); - jsonWriter.WriteRaw(XsrfPrefix); + jsonWriter.WriteRaw(XsrfPrefix); - return jsonWriter; - } - - protected static JsonSerializerSettings RegisterJsonConverters(JsonSerializerSettings serializerSettings) - { - serializerSettings.Converters.Add(new StringEnumConverter()); - serializerSettings.Converters.Add(new UdiJsonConverter()); - - return serializerSettings; - } + return jsonWriter; } } diff --git a/src/Umbraco.Web.Common/Formatters/IgnoreRequiredAttributesResolver.cs b/src/Umbraco.Web.Common/Formatters/IgnoreRequiredAttributesResolver.cs index 1988afcaef..cee92be436 100644 --- a/src/Umbraco.Web.Common/Formatters/IgnoreRequiredAttributesResolver.cs +++ b/src/Umbraco.Web.Common/Formatters/IgnoreRequiredAttributesResolver.cs @@ -1,18 +1,17 @@ -using System.Reflection; +using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Umbraco.Cms.Web.Common.Formatters +namespace Umbraco.Cms.Web.Common.Formatters; + +public class IgnoreRequiredAttributesResolver : DefaultContractResolver { - public class IgnoreRequiredAttributesResolver : DefaultContractResolver + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var property = base.CreateProperty(member, memberSerialization); + JsonProperty property = base.CreateProperty(member, memberSerialization); - property.Required = Required.Default; + property.Required = Required.Default; - return property; - } + return property; } } diff --git a/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs b/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs index 59f5bac85a..eaa08608d6 100644 --- a/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs @@ -6,12 +6,12 @@ using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Web.Common.Hosting; /// -/// Umbraco specific extensions for the interface. +/// Umbraco specific extensions for the interface. /// public static class HostBuilderExtensions { /// - /// Configures an existing with defaults for an Umbraco application. + /// Configures an existing with defaults for an Umbraco application. /// public static IHostBuilder ConfigureUmbracoDefaults(this IHostBuilder builder) { @@ -19,8 +19,8 @@ public static class HostBuilderExtensions builder.ConfigureAppConfiguration(config => config.AddJsonFile( "appsettings.Local.json", - optional: true, - reloadOnChange: true)); + true, + true)); #endif builder.ConfigureLogging(x => x.ClearProviders()); diff --git a/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs b/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs index c460cf8f03..afe4e78906 100644 --- a/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs +++ b/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs @@ -15,13 +15,17 @@ internal class UmbracoHostBuilderDecorator : IHostBuilder _onBuild = onBuild; } - public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + public IDictionary Properties => _inner.Properties; + + public IHostBuilder + ConfigureAppConfiguration(Action configureDelegate) { _inner.ConfigureAppConfiguration(configureDelegate); return this; } - public IHostBuilder ConfigureContainer(Action configureDelegate) + public IHostBuilder ConfigureContainer( + Action configureDelegate) { _inner.ConfigureContainer(configureDelegate); return this; @@ -46,15 +50,14 @@ internal class UmbracoHostBuilderDecorator : IHostBuilder return this; } - public IHostBuilder UseServiceProviderFactory(Func> factory) + public IHostBuilder UseServiceProviderFactory( + Func> factory) where TContainerBuilder : notnull { _inner.UseServiceProviderFactory(factory); return this; } - public IDictionary Properties => _inner.Properties; - public IHost Build() { IHost host = _inner.Build(); diff --git a/src/Umbraco.Web.Common/IUmbracoHelperAccessor.cs b/src/Umbraco.Web.Common/IUmbracoHelperAccessor.cs index b170546005..c6867d634a 100644 --- a/src/Umbraco.Web.Common/IUmbracoHelperAccessor.cs +++ b/src/Umbraco.Web.Common/IUmbracoHelperAccessor.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Web.Common +namespace Umbraco.Cms.Web.Common; + +public interface IUmbracoHelperAccessor { - public interface IUmbracoHelperAccessor - { - bool TryGetUmbracoHelper([MaybeNullWhen(false)] out UmbracoHelper umbracoHelper); - } + bool TryGetUmbracoHelper([MaybeNullWhen(false)] out UmbracoHelper umbracoHelper); } diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 85ecf2d844..0d48ac4cc4 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Numerics; using Microsoft.Extensions.Logging; @@ -10,83 +8,81 @@ using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Processors; -namespace Umbraco.Cms.Web.Common.ImageProcessors +namespace Umbraco.Cms.Web.Common.ImageProcessors; + +/// +/// Allows the cropping of images. +/// +public class CropWebProcessor : IImageWebProcessor { /// - /// Allows the cropping of images. + /// The command constant for the crop coordinates. /// - public class CropWebProcessor : IImageWebProcessor + public const string Coordinates = "cc"; + + /// + /// The command constant for the resize orientation handling mode. + /// + public const string Orient = "orient"; + + /// + public IEnumerable Commands { get; } = new[] { Coordinates, Orient }; + + /// + public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) { - /// - /// The command constant for the crop coordinates. - /// - public const string Coordinates = "cc"; - - /// - /// The command constant for the resize orientation handling mode. - /// - public const string Orient = "orient"; - - /// - public IEnumerable Commands { get; } = new[] + Rectangle? cropRectangle = GetCropRectangle(image, commands, parser, culture); + if (cropRectangle.HasValue) { - Coordinates, - Orient - }; - - /// - public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) - { - Rectangle? cropRectangle = GetCropRectangle(image, commands, parser, culture); - if (cropRectangle.HasValue) - { - image.Image.Mutate(x => x.Crop(cropRectangle.Value)); - } - - return image; + image.Image.Mutate(x => x.Crop(cropRectangle.Value)); } - /// - public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; + return image; + } - private static Rectangle? GetCropRectangle(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + /// + public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => + false; + + private static Rectangle? GetCropRectangle(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + { + var coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); + if (coordinates.Length != 4 || + (coordinates[0] == 0 && coordinates[1] == 0 && coordinates[2] == 0 && coordinates[3] == 0)) { - float[] coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); - if (coordinates.Length != 4 || - (coordinates[0] == 0 && coordinates[1] == 0 && coordinates[2] == 0 && coordinates[3] == 0)) - { - return null; - } - - // The right and bottom values are actually the distance from those sides, so convert them into real coordinates and transform to correct orientation - float left = Math.Clamp(coordinates[0], 0, 1); - float top = Math.Clamp(coordinates[1], 0, 1); - float right = Math.Clamp(1 - coordinates[2], 0, 1); - float bottom = Math.Clamp(1 - coordinates[3], 0, 1); - ushort orientation = GetExifOrientation(image, commands, parser, culture); - Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation); - Vector2 xy2 = ExifOrientationUtilities.Transform(new Vector2(right, bottom), Vector2.Zero, Vector2.One, orientation); - - // Scale points to a pixel based rectangle - Size size = image.Image.Size(); - - return Rectangle.Round(RectangleF.FromLTRB( - MathF.Min(xy1.X, xy2.X) * size.Width, - MathF.Min(xy1.Y, xy2.Y) * size.Height, - MathF.Max(xy1.X, xy2.X) * size.Width, - MathF.Max(xy1.Y, xy2.Y) * size.Height)); + return null; } - private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + // The right and bottom values are actually the distance from those sides, so convert them into real coordinates and transform to correct orientation + var left = Math.Clamp(coordinates[0], 0, 1); + var top = Math.Clamp(coordinates[1], 0, 1); + var right = Math.Clamp(1 - coordinates[2], 0, 1); + var bottom = Math.Clamp(1 - coordinates[3], 0, 1); + var orientation = GetExifOrientation(image, commands, parser, culture); + Vector2 xy1 = + ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation); + Vector2 xy2 = + ExifOrientationUtilities.Transform(new Vector2(right, bottom), Vector2.Zero, Vector2.One, orientation); + + // Scale points to a pixel based rectangle + Size size = image.Image.Size(); + + return Rectangle.Round(RectangleF.FromLTRB( + MathF.Min(xy1.X, xy2.X) * size.Width, + MathF.Min(xy1.Y, xy2.Y) * size.Height, + MathF.Max(xy1.X, xy2.X) * size.Width, + MathF.Max(xy1.Y, xy2.Y) * size.Height)); + } + + private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + { + if (commands.Contains(Orient) && !parser.ParseValue(commands.GetValueOrDefault(Orient), culture)) { - if (commands.Contains(Orient) && !parser.ParseValue(commands.GetValueOrDefault(Orient), culture)) - { - return ExifOrientationMode.Unknown; - } - - image.TryGetExifOrientation(out ushort orientation); - - return orientation; + return ExifOrientationMode.Unknown; } + + image.TryGetExifOrientation(out var orientation); + + return orientation; } } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs index fbe0aaacb5..3e84774c7b 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs @@ -2,59 +2,58 @@ // See LICENSE for more details. using System.Globalization; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Localization +namespace Umbraco.Cms.Web.Common.Localization; + +/// +/// Sets the request culture to the culture of the back office user if one is determined to be in the request +/// +public class UmbracoBackOfficeIdentityCultureProvider : RequestCultureProvider { + private readonly RequestLocalizationOptions _localizationOptions; + private readonly object _locker = new(); + /// - /// Sets the request culture to the culture of the back office user if one is determined to be in the request + /// Initializes a new instance of the class. /// - public class UmbracoBackOfficeIdentityCultureProvider : RequestCultureProvider + public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions) => + _localizationOptions = localizationOptions; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) { - private readonly RequestLocalizationOptions _localizationOptions; - private readonly object _locker = new object(); + CultureInfo? culture = httpContext.User.Identity?.GetCulture(); - /// - /// Initializes a new instance of the class. - /// - public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions) => _localizationOptions = localizationOptions; - - /// - public override Task DetermineProviderCultureResult(HttpContext httpContext) + if (culture is null) { - CultureInfo? culture = httpContext.User.Identity?.GetCulture(); + return NullProviderCultureResult; + } - if (culture is null) + lock (_locker) + { + // We need to dynamically change the supported cultures since we won't ever know what languages are used since + // they are dynamic within Umbraco. We have to handle this for both UI and Region cultures, in case people run different region and UI languages + var cultureExists = _localizationOptions.SupportedCultures?.Contains(culture) ?? false; + + if (!cultureExists) { - return NullProviderCultureResult; + // add this as a supporting culture + _localizationOptions.SupportedCultures?.Add(culture); } - lock (_locker) + var uiCultureExists = _localizationOptions.SupportedCultures?.Contains(culture) ?? false; + + if (!uiCultureExists) { - // We need to dynamically change the supported cultures since we won't ever know what languages are used since - // they are dynamic within Umbraco. We have to handle this for both UI and Region cultures, in case people run different region and UI languages - var cultureExists = _localizationOptions.SupportedCultures?.Contains(culture) ?? false; - - if (!cultureExists) - { - // add this as a supporting culture - _localizationOptions.SupportedCultures?.Add(culture); - } - - var uiCultureExists = _localizationOptions.SupportedCultures?.Contains(culture) ?? false; - - if (!uiCultureExists) - { - // add this as a supporting culture - _localizationOptions.SupportedUICultures?.Add(culture); - } - - return Task.FromResult(new ProviderCultureResult(culture.Name)); + // add this as a supporting culture + _localizationOptions.SupportedUICultures?.Add(culture); } + + return Task.FromResult(new ProviderCultureResult(culture.Name)); } } } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs index 64ce9a6f8e..a3252c66e6 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs @@ -1,7 +1,4 @@ -using System; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; @@ -9,63 +6,64 @@ using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Common.Localization +namespace Umbraco.Cms.Web.Common.Localization; + +/// +/// Sets the request culture to the culture of the if one is found in the request +/// +public class UmbracoPublishedContentCultureProvider : RequestCultureProvider { + private readonly RequestLocalizationOptions _localizationOptions; + private readonly object _locker = new(); + /// - /// Sets the request culture to the culture of the if one is found in the request + /// Initializes a new instance of the class. /// - public class UmbracoPublishedContentCultureProvider : RequestCultureProvider + public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions) => + _localizationOptions = localizationOptions; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) { - private readonly RequestLocalizationOptions _localizationOptions; - private readonly object _locker = new object(); - - /// - /// Initializes a new instance of the class. - /// - public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions) => _localizationOptions = localizationOptions; - - /// - public override Task DetermineProviderCultureResult(HttpContext httpContext) + UmbracoRouteValues? routeValues = httpContext.Features.Get(); + if (routeValues != null) { - UmbracoRouteValues? routeValues = httpContext.Features.Get(); - if (routeValues != null) + var culture = routeValues.PublishedRequest.Culture; + if (culture != null) { - string? culture = routeValues.PublishedRequest?.Culture; - if (culture != null) + lock (_locker) { - lock (_locker) + // We need to dynamically change the supported cultures since we won't ever know what languages are used since + // they are dynamic within Umbraco. We have to handle this for both UI and Region cultures, in case people run different region and UI languages + // This code to check existence is borrowed from aspnetcore to avoid creating a CultureInfo + // https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165 + CultureInfo? existingCulture = _localizationOptions.SupportedCultures?.FirstOrDefault( + supportedCulture => + StringSegment.Equals(supportedCulture.Name, culture, StringComparison.OrdinalIgnoreCase)); + + if (existingCulture == null) { - // We need to dynamically change the supported cultures since we won't ever know what languages are used since - // they are dynamic within Umbraco. We have to handle this for both UI and Region cultures, in case people run different region and UI languages - // This code to check existence is borrowed from aspnetcore to avoid creating a CultureInfo - // https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165 - CultureInfo? existingCulture = _localizationOptions.SupportedCultures?.FirstOrDefault(supportedCulture => - StringSegment.Equals(supportedCulture.Name, culture, StringComparison.OrdinalIgnoreCase)); - - if (existingCulture == null) - { - // add this as a supporting culture - var ci = CultureInfo.GetCultureInfo(culture); - _localizationOptions.SupportedCultures?.Add(ci); - } - - CultureInfo? existingUICulture = _localizationOptions.SupportedUICultures?.FirstOrDefault(supportedCulture => - StringSegment.Equals(supportedCulture.Name, culture, StringComparison.OrdinalIgnoreCase)); - - if (existingUICulture == null) - { - // add this as a supporting culture - var ci = CultureInfo.GetCultureInfo(culture); - _localizationOptions.SupportedUICultures?.Add(ci); - } + // add this as a supporting culture + var ci = CultureInfo.GetCultureInfo(culture); + _localizationOptions.SupportedCultures?.Add(ci); } - return Task.FromResult(new ProviderCultureResult(culture)); - } - } + CultureInfo? existingUICulture = _localizationOptions.SupportedUICultures?.FirstOrDefault( + supportedCulture => + StringSegment.Equals(supportedCulture.Name, culture, StringComparison.OrdinalIgnoreCase)); - return NullProviderCultureResult; + if (existingUICulture == null) + { + // add this as a supporting culture + var ci = CultureInfo.GetCultureInfo(culture); + _localizationOptions.SupportedUICultures?.Add(ci); + } + } + + return Task.FromResult(new ProviderCultureResult(culture)); + } } + return NullProviderCultureResult; } } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs index 9cbac8fcc8..802a68607d 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs @@ -1,40 +1,30 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Localization +namespace Umbraco.Cms.Web.Common.Localization; + +/// +/// Custom Umbraco options configuration for +/// +public class UmbracoRequestLocalizationOptions : IConfigureOptions { + private readonly GlobalSettings _globalSettings; + /// - /// Custom Umbraco options configuration for + /// Initializes a new instance of the class. /// - public class UmbracoRequestLocalizationOptions : IConfigureOptions + public UmbracoRequestLocalizationOptions(IOptions globalSettings) => + _globalSettings = globalSettings.Value; + + /// + public void Configure(RequestLocalizationOptions options) { - private GlobalSettings _globalSettings; + // set the default culture to what is in config + options.DefaultRequestCulture = new RequestCulture(_globalSettings.DefaultUILanguage); - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestLocalizationOptions(IOptions globalSettings) - { - _globalSettings = globalSettings.Value; - } - - /// - public void Configure(RequestLocalizationOptions options) - { - // set the default culture to what is in config - options.DefaultRequestCulture = new RequestCulture(_globalSettings.DefaultUILanguage); - - // add a custom provider - if (options.RequestCultureProviders == null) - { - options.RequestCultureProviders = new List(); - } - - options.RequestCultureProviders.Insert(0, new UmbracoBackOfficeIdentityCultureProvider(options)); - options.RequestCultureProviders.Insert(1, new UmbracoPublishedContentCultureProvider(options)); - } + options.RequestCultureProviders.Insert(0, new UmbracoBackOfficeIdentityCultureProvider(options)); + options.RequestCultureProviders.Insert(1, new UmbracoPublishedContentCultureProvider(options)); } } diff --git a/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs b/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs index 9086cd04ec..7886e38614 100644 --- a/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs +++ b/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs @@ -7,12 +7,14 @@ namespace Umbraco.Cms.Web.Common.Logging.Enrichers; internal class ApplicationIdEnricher : ILogEventEnricher { - private readonly IApplicationDiscriminator _applicationDiscriminator; public const string ApplicationIdProperty = "ApplicationId"; + private readonly IApplicationDiscriminator _applicationDiscriminator; public ApplicationIdEnricher(IApplicationDiscriminator applicationDiscriminator) => _applicationDiscriminator = applicationDiscriminator; public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) => - logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty(ApplicationIdProperty, _applicationDiscriminator.GetApplicationId())); + logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty( + ApplicationIdProperty, + _applicationDiscriminator.GetApplicationId())); } diff --git a/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs b/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs index 86402afd45..d0ec659526 100644 --- a/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs +++ b/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs @@ -4,7 +4,7 @@ using Serilog.Events; namespace Umbraco.Cms.Web.Common.Logging.Enrichers; /// -/// NoOp but useful for tricks to avoid disposal of the global logger. +/// NoOp but useful for tricks to avoid disposal of the global logger. /// internal class NoopEnricher : ILogEventEnricher { diff --git a/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs b/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs index 2a6845906b..c146198097 100644 --- a/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs +++ b/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs @@ -1,18 +1,17 @@ -using System; using Serilog; using Serilog.Extensions.Hosting; namespace Umbraco.Cms.Web.Common.Logging; /// -/// HACK: -/// Ensures freeze is only called a single time even when resolving a logger from the snapshot container -/// built for . +/// HACK: +/// Ensures freeze is only called a single time even when resolving a logger from the snapshot container +/// built for . /// internal class RegisteredReloadableLogger { - private static bool s_frozen; - private static object s_frozenLock = new(); + private static readonly object FrozenLock = new(); + private static bool frozen; private readonly ReloadableLogger _logger; public RegisteredReloadableLogger(ReloadableLogger? logger) => @@ -22,9 +21,9 @@ internal class RegisteredReloadableLogger public void Reload(Func cfg) { - lock (s_frozenLock) + lock (FrozenLock) { - if (s_frozen) + if (frozen) { Logger.Debug("ReloadableLogger has already been frozen, unable to reload, NOOP."); return; @@ -33,8 +32,7 @@ internal class RegisteredReloadableLogger _logger.Reload(cfg); _logger.Freeze(); - s_frozen = true; + frozen = true; } } } - diff --git a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs index 3c4bebc9c4..77eee873da 100644 --- a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,437 +8,500 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Macros; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Web.Common.Macros +namespace Umbraco.Cms.Web.Common.Macros; + +public class MacroRenderer : IMacroRenderer { - public class MacroRenderer : IMacroRenderer + private readonly AppCaches _appCaches; + private readonly ICookieManager _cookieManager; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly ILogger _logger; + private readonly IMacroService _macroService; + private readonly PartialViewMacroEngine _partialViewMacroEngine; + private readonly IProfilingLogger _profilingLogger; + private readonly IRequestAccessor _requestAccessor; + private readonly ISessionManager _sessionManager; + private readonly ILocalizedTextService _textService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private ContentSettings _contentSettings; + + [Obsolete("Please use constructor that takes an IWebHostEnvironment instead")] + public MacroRenderer( + IProfilingLogger profilingLogger, + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsMonitor contentSettings, + ILocalizedTextService textService, + AppCaches appCaches, + IMacroService macroService, + IHostingEnvironment hostingEnvironment, + ICookieManager cookieManager, + ISessionManager sessionManager, + IRequestAccessor requestAccessor, + PartialViewMacroEngine partialViewMacroEngine, + IHttpContextAccessor httpContextAccessor) + : this( + profilingLogger, + logger, + umbracoContextAccessor, + contentSettings, + textService, + appCaches, + macroService, + cookieManager, + sessionManager, + requestAccessor, + partialViewMacroEngine, + httpContextAccessor, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private ContentSettings _contentSettings; - private readonly ILocalizedTextService _textService; - private readonly AppCaches _appCaches; - private readonly IMacroService _macroService; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ICookieManager _cookieManager; - private readonly ISessionManager _sessionManager; - private readonly IRequestAccessor _requestAccessor; - private readonly PartialViewMacroEngine _partialViewMacroEngine; - private readonly IHttpContextAccessor _httpContextAccessor; + } - public MacroRenderer( - IProfilingLogger profilingLogger, - ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, - IOptionsMonitor contentSettings, - ILocalizedTextService textService, - AppCaches appCaches, - IMacroService macroService, - IHostingEnvironment hostingEnvironment, - ICookieManager cookieManager, - ISessionManager sessionManager, - IRequestAccessor requestAccessor, - PartialViewMacroEngine partialViewMacroEngine, - IHttpContextAccessor httpContextAccessor) + public MacroRenderer( + IProfilingLogger profilingLogger, + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsMonitor contentSettings, + ILocalizedTextService textService, + AppCaches appCaches, + IMacroService macroService, + ICookieManager cookieManager, + ISessionManager sessionManager, + IRequestAccessor requestAccessor, + PartialViewMacroEngine partialViewMacroEngine, + IHttpContextAccessor httpContextAccessor, + IWebHostEnvironment webHostEnvironment) + { + _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + _logger = logger; + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); + _textService = textService; + _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); + _cookieManager = cookieManager; + _sessionManager = sessionManager; + _requestAccessor = requestAccessor; + _partialViewMacroEngine = partialViewMacroEngine; + _httpContextAccessor = httpContextAccessor; + _webHostEnvironment = webHostEnvironment; + + contentSettings.OnChange(x => _contentSettings = x); + } + + #region Execution helpers + + // parses attribute value looking for [@requestKey], [%sessionKey] + // supports fallbacks eg "[@requestKey],[%sessionKey],1234" + private string? ParseAttribute(string? attributeValue) + { + if (attributeValue is null) { - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); - _logger = logger; - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); - _textService = textService; - _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); - _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _cookieManager = cookieManager; - _sessionManager = sessionManager; - _requestAccessor = requestAccessor; - _partialViewMacroEngine = partialViewMacroEngine; - _httpContextAccessor = httpContextAccessor; - - contentSettings.OnChange(x => _contentSettings = x); + return attributeValue; } - #region MacroContent cache - - // gets this macro content cache identifier - private async Task GetContentCacheIdentifier(MacroModel model, int pageId, string cultureName) + // check for potential querystring/cookie variables + attributeValue = attributeValue.Trim(); + if (attributeValue.StartsWith("[") == false) { - var id = new StringBuilder(); - - var alias = model.Alias; - id.AppendFormat("{0}-", alias); - //always add current culture to the key to allow variants to have different cache results - if (!string.IsNullOrEmpty(cultureName)) - { - // are there any unusual culture formats we'd need to handle? - id.AppendFormat("{0}-", cultureName); - } - - if (model.CacheByPage) - id.AppendFormat("{0}-", pageId); - - if (model.CacheByMember) - { - object key = 0; - - if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false) - { - var memberManager = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(); - var member = await memberManager.GetCurrentMemberAsync(); - if (member is not null) - { - key = member.Key; - } - } - - id.AppendFormat("m{0}-", key); - } - - foreach (var value in model.Properties.Select(x => x.Value)) - id.AppendFormat("{0}-", value?.Length <= 255 ? value : value?.Substring(0, 255)); - - return id.ToString(); + return attributeValue; } - // gets this macro content from the cache - // ensuring that it is appropriate to use the cache - private MacroContent? GetMacroContentFromCache(MacroModel model) - { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } - // only if cache is enabled - if (umbracoContext.InPreviewMode || model.CacheDuration <= 0) - return null; + var tokens = attributeValue.Split(Core.Constants.CharArrays.Comma).Select(x => x.Trim()).ToArray(); - var cache = _appCaches.RuntimeCache; - var macroContent = cache.GetCacheItem(CacheKeys.MacroContentCacheKey + model.CacheIdentifier); - - if (macroContent == null) - return null; - - _logger.LogDebug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); - - // ensure that the source has not changed - // note: does not handle dependencies, and never has - var macroSource = GetMacroFile(model); // null if macro is not file-based - if (macroSource != null) - { - if (macroSource.Exists == false) - { - _logger.LogDebug("Macro source does not exist anymore, ignore cache."); - return null; - } - - if (macroContent.Date < macroSource.LastWriteTime) - { - _logger.LogDebug("Macro source has changed, ignore cache."); - return null; - } - } - - return macroContent; - } - - // stores macro content into the cache - private async Task AddMacroContentToCacheAsync(MacroModel model, MacroContent macroContent) - { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - - // only if cache is enabled - if (umbracoContext.InPreviewMode || model.CacheDuration <= 0) - return; - - // just make sure... - if (macroContent == null) - return; - - // do not cache if it should cache by member and there's not member - if (model.CacheByMember) - { - var memberManager = _httpContextAccessor.HttpContext?.RequestServices.GetRequiredService(); - MemberIdentityUser? member = await memberManager?.GetCurrentMemberAsync()!; - if (member is null) - { - return; - } - } - - // remember when we cache the content - macroContent.Date = DateTime.Now; - - var cache = _appCaches.RuntimeCache; - cache.Insert( - CacheKeys.MacroContentCacheKey + model.CacheIdentifier, - () => macroContent, - new TimeSpan(0, 0, model.CacheDuration) - ); - - _logger.LogDebug("Macro content saved to cache '{MacroCacheId}'", model.CacheIdentifier); - } - - // gets the macro source file name - // null if the macro is not file-based, or not supported - internal static string? GetMacroFileName(MacroModel model) - { - string? filename = model.MacroSource; // partial views are saved with their full virtual path - - return string.IsNullOrEmpty(filename) ? null : filename; - } - - // gets the macro source file - // null if macro is not file-based - private FileInfo? GetMacroFile(MacroModel model) - { - var filename = GetMacroFileName(model); - if (filename == null) - return null; - - var mapped = _hostingEnvironment.MapPathContentRoot(filename); - if (mapped == null) - return null; - - var file = new FileInfo(mapped); - return file.Exists ? file : null; - } - - // updates the model properties values according to the attributes - private static void UpdateMacroModelProperties(MacroModel model, IDictionary? macroParams) - { - foreach (var prop in model.Properties) - { - var key = prop.Key.ToLowerInvariant(); - prop.Value = macroParams != null && macroParams.ContainsKey(key) - ? macroParams[key]?.ToString() ?? string.Empty - : string.Empty; - } - } - #endregion - - #region Render/Execute - - public async Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams) - { - var m = _appCaches.RuntimeCache.GetCacheItem(CacheKeys.MacroFromAliasCacheKey + macroAlias, () => _macroService.GetByAlias(macroAlias)); - - if (m == null) - throw new InvalidOperationException("No macro found by alias " + macroAlias); - - var macro = new MacroModel(m); - - UpdateMacroModelProperties(macro, macroParams); - return await RenderAsync(macro, content); - } - - private async Task RenderAsync(MacroModel macro, IPublishedContent? content) - { - if (content == null) - throw new ArgumentNullException(nameof(content)); - - var macroInfo = $"Render Macro: {macro.Name}, cache: {macro.CacheDuration}"; - using (_profilingLogger.DebugDuration(macroInfo, "Rendered Macro.")) - { - // parse macro parameters ie replace the special [#key], [$key], etc. syntaxes - foreach (var prop in macro.Properties) - prop.Value = ParseAttribute(prop.Value); - - var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; - macro.CacheIdentifier = await GetContentCacheIdentifier(macro, content.Id, cultureName); - - // get the macro from cache if it is there - var macroContent = GetMacroContentFromCache(macro); - - // macroContent.IsEmpty may be true, meaning the macro produces no output, - // but still can be cached because its execution did not trigger any error. - // so we need to actually render, only if macroContent is null - if (macroContent != null) - return macroContent; - - // this will take care of errors - // it may throw, if we actually want to throw, so better not - // catch anything here and let the exception be thrown - var attempt = ExecuteMacroOfType(macro, content); - - // by convention ExecuteMacroByType must either throw or return a result - // just check to avoid internal errors - macroContent = attempt.Result; - if (macroContent == null) - throw new Exception("Internal error, ExecuteMacroOfType returned no content."); - - // add to cache if render is successful - // content may be empty but that's not an issue - if (attempt.Success) - { - // write to cache (if appropriate) - await AddMacroContentToCacheAsync(macro, macroContent); - } - - return macroContent; - } - } - - /// - /// Executes a macro of a given type. - /// - private Attempt ExecuteMacroWithErrorWrapper(MacroModel macro, string msgIn, string msgOut, Func getMacroContent, Func msgErr) - { - using (_profilingLogger.DebugDuration(msgIn, msgOut)) - { - return ExecuteProfileMacroWithErrorWrapper(macro, msgIn, getMacroContent, msgErr); - } - } - - /// - /// Executes a macro of a given type. - /// - private Attempt ExecuteProfileMacroWithErrorWrapper(MacroModel macro, string msgIn, Func getMacroContent, Func msgErr) - { - try - { - return Attempt.Succeed(getMacroContent()); - } - catch (Exception e) - { - _logger.LogWarning(e, "Failed {MsgIn}", msgIn); - - var macroErrorEventArgs = new MacroErrorEventArgs - { - Name = macro.Name, - Alias = macro.Alias, - MacroSource = macro.MacroSource, - Exception = e, - Behaviour = _contentSettings.MacroErrors - }; - - switch (macroErrorEventArgs.Behaviour) - { - case MacroErrorBehaviour.Inline: - // do not throw, eat the exception, display the trace error message - return Attempt.Fail(new MacroContent { Text = msgErr() }, e); - case MacroErrorBehaviour.Silent: - // do not throw, eat the exception, do not display anything - return Attempt.Fail(new MacroContent { Text = string.Empty }, e); - case MacroErrorBehaviour.Content: - // do not throw, eat the exception, display the custom content - return Attempt.Fail(new MacroContent { Text = macroErrorEventArgs.Html ?? string.Empty }, e); - //case MacroErrorBehaviour.Throw: - default: - // see http://issues.umbraco.org/issue/U4-497 at the end - // throw the original exception - throw; - } - } - } - - /// - /// Executes a macro. - /// - /// Returns an attempt that is successful if the macro ran successfully. If the macro failed - /// to run properly, the attempt fails, though it may contain a content. But for instance that content - /// should not be cached. In that case the attempt may also contain an exception. - private Attempt ExecuteMacroOfType(MacroModel model, IPublishedContent content) - { - if (model == null) - { - throw new ArgumentNullException(nameof(model)); - } - - // ensure that we are running against a published node (ie available in XML) - // that may not be the case if the macro is embedded in a RTE of an unpublished document - - if (content == null) - { - return Attempt.Fail(new MacroContent { Text = "[macro failed (no content)]" }); - } - - - return ExecuteMacroWithErrorWrapper(model, - $"Executing PartialView: MacroSource=\"{model.MacroSource}\".", - "Executed PartialView.", - () => _partialViewMacroEngine.Execute(model, content), - () => _textService.Localize("errors", "macroErrorLoadingPartialView", new[] { model.MacroSource })); - } - - - #endregion - - #region Execution helpers - - // parses attribute value looking for [@requestKey], [%sessionKey] - // supports fallbacks eg "[@requestKey],[%sessionKey],1234" - private string? ParseAttribute(string? attributeValue) - { - if (attributeValue is null) - { - return attributeValue; - } - // check for potential querystring/cookie variables - attributeValue = attributeValue.Trim(); - if (attributeValue.StartsWith("[") == false) - return attributeValue; - - var tokens = attributeValue.Split(Core.Constants.CharArrays.Comma).Select(x => x.Trim()).ToArray(); - - // ensure we only process valid input ie each token must be [?x] and not eg a json array - // like [1,2,3] which we don't want to parse - however the last one can be a literal, so - // don't check on the last one which can be just anything - check all previous tokens - - char[] validTypes = { '@', '%' }; - if (tokens.Take(tokens.Length - 1).Any(x => + // ensure we only process valid input ie each token must be [?x] and not eg a json array + // like [1,2,3] which we don't want to parse - however the last one can be a literal, so + // don't check on the last one which can be just anything - check all previous tokens + char[] validTypes = { '@', '%' }; + if (tokens.Take(tokens.Length - 1).Any(x => x.Length < 4 // ie "[?x]".Length - too short || x[0] != '[' // starts with [ || x[x.Length - 1] != ']' // ends with ] || validTypes.Contains(x[1]) == false)) - { - return attributeValue; - } - - foreach (var token in tokens) - { - var isToken = token.Length > 4 && token[0] == '[' && token[token.Length - 1] == ']' && validTypes.Contains(token[1]); - - if (isToken == false) - { - // anything that is not a token is a value, use it - attributeValue = token; - break; - } - - var type = token[1]; - var name = token.Substring(2, token.Length - 3); - - switch (type) - { - case '@': - attributeValue = _requestAccessor.GetRequestValue(name); - break; - case '%': - attributeValue = _sessionManager.GetSessionValue(name); - if (string.IsNullOrEmpty(attributeValue)) - attributeValue = _cookieManager.GetCookieValue(name); - break; - } - - attributeValue = attributeValue?.Trim(); - if (string.IsNullOrEmpty(attributeValue) == false) - break; // got a value, use it - } - + { return attributeValue; } - #endregion + foreach (var token in tokens) + { + var isToken = token.Length > 4 && token[0] == '[' && token[token.Length - 1] == ']' && + validTypes.Contains(token[1]); + if (isToken == false) + { + // anything that is not a token is a value, use it + attributeValue = token; + break; + } + + var type = token[1]; + var name = token.Substring(2, token.Length - 3); + + switch (type) + { + case '@': + attributeValue = _requestAccessor.GetRequestValue(name); + break; + case '%': + attributeValue = _sessionManager.GetSessionValue(name); + if (string.IsNullOrEmpty(attributeValue)) + { + attributeValue = _cookieManager.GetCookieValue(name); + } + + break; + } + + attributeValue = attributeValue?.Trim(); + if (string.IsNullOrEmpty(attributeValue) == false) + { + break; // got a value, use it + } + } + + return attributeValue; } + #endregion + + #region MacroContent cache + + // gets this macro content cache identifier + private async Task GetContentCacheIdentifier(MacroModel model, int pageId, string cultureName) + { + var id = new StringBuilder(); + + var alias = model.Alias; + id.AppendFormat("{0}-", alias); + + // always add current culture to the key to allow variants to have different cache results + if (!string.IsNullOrEmpty(cultureName)) + { + // are there any unusual culture formats we'd need to handle? + id.AppendFormat("{0}-", cultureName); + } + + if (model.CacheByPage) + { + id.AppendFormat("{0}-", pageId); + } + + if (model.CacheByMember) + { + object key = 0; + + if (_httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false) + { + IMemberManager memberManager = + _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(); + MemberIdentityUser? member = await memberManager.GetCurrentMemberAsync(); + if (member is not null) + { + key = member.Key; + } + } + + id.AppendFormat("m{0}-", key); + } + + foreach (var value in model.Properties.Select(x => x.Value)) + { + id.AppendFormat("{0}-", value?.Length <= 255 ? value : value?.Substring(0, 255)); + } + + return id.ToString(); + } + + // gets this macro content from the cache + // ensuring that it is appropriate to use the cache + private MacroContent? GetMacroContentFromCache(MacroModel model) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return null; + } + + // only if cache is enabled + if (umbracoContext.InPreviewMode || model.CacheDuration <= 0) + { + return null; + } + + IAppPolicyCache cache = _appCaches.RuntimeCache; + MacroContent? macroContent = + cache.GetCacheItem(CacheKeys.MacroContentCacheKey + model.CacheIdentifier); + + if (macroContent == null) + { + return null; + } + + _logger.LogDebug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); + + // ensure that the source has not changed + // note: does not handle dependencies, and never has + FileInfo? macroSource = GetMacroFile(model); // null if macro is not file-based + if (macroSource != null) + { + if (macroSource.Exists == false) + { + _logger.LogDebug("Macro source does not exist anymore, ignore cache."); + return null; + } + + if (macroContent.Date < macroSource.LastWriteTime) + { + _logger.LogDebug("Macro source has changed, ignore cache."); + return null; + } + } + + return macroContent; + } + + // stores macro content into the cache + private async Task AddMacroContentToCacheAsync(MacroModel model, MacroContent macroContent) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // only if cache is enabled + if (umbracoContext.InPreviewMode || model.CacheDuration <= 0) + { + return; + } + + // do not cache if it should cache by member and there's not member + if (model.CacheByMember) + { + IMemberManager? memberManager = + _httpContextAccessor.HttpContext?.RequestServices.GetRequiredService(); + MemberIdentityUser? member = await memberManager?.GetCurrentMemberAsync()!; + if (member is null) + { + return; + } + } + + // remember when we cache the content + macroContent.Date = DateTime.Now; + + IAppPolicyCache cache = _appCaches.RuntimeCache; + cache.Insert( + CacheKeys.MacroContentCacheKey + model.CacheIdentifier, + () => macroContent, + new TimeSpan(0, 0, model.CacheDuration)); + + _logger.LogDebug("Macro content saved to cache '{MacroCacheId}'", model.CacheIdentifier); + } + + // gets the macro source file name + // null if the macro is not file-based, or not supported + internal static string? GetMacroFileName(MacroModel model) + { + var filename = model.MacroSource; // partial views are saved with their full virtual path + + return string.IsNullOrEmpty(filename) ? null : filename; + } + + // gets the macro source file + // null if macro is not file-based + private FileInfo? GetMacroFile(MacroModel model) + { + var filename = GetMacroFileName(model); + if (filename == null) + { + return null; + } + + var mapped = _webHostEnvironment.MapPathContentRoot(filename); + + var file = new FileInfo(mapped); + return file.Exists ? file : null; + } + + // updates the model properties values according to the attributes + private static void UpdateMacroModelProperties(MacroModel model, IDictionary? macroParams) + { + foreach (MacroPropertyModel prop in model.Properties) + { + var key = prop.Key.ToLowerInvariant(); + prop.Value = macroParams != null && macroParams.ContainsKey(key) + ? macroParams[key]?.ToString() ?? string.Empty + : string.Empty; + } + } + + #endregion + + #region Render/Execute + + public async Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams) + { + IMacro? m = _appCaches.RuntimeCache.GetCacheItem(CacheKeys.MacroFromAliasCacheKey + macroAlias, () => _macroService.GetByAlias(macroAlias)); + + if (m == null) + { + throw new InvalidOperationException("No macro found by alias " + macroAlias); + } + + var macro = new MacroModel(m); + + UpdateMacroModelProperties(macro, macroParams); + return await RenderAsync(macro, content); + } + + private async Task RenderAsync(MacroModel macro, IPublishedContent? content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var macroInfo = $"Render Macro: {macro.Name}, cache: {macro.CacheDuration}"; + using (_profilingLogger.DebugDuration(macroInfo, "Rendered Macro.")) + { + // parse macro parameters ie replace the special [#key], [$key], etc. syntaxes + foreach (MacroPropertyModel prop in macro.Properties) + { + prop.Value = ParseAttribute(prop.Value); + } + + var cultureName = Thread.CurrentThread.CurrentUICulture.Name; + macro.CacheIdentifier = await GetContentCacheIdentifier(macro, content.Id, cultureName); + + // get the macro from cache if it is there + MacroContent? macroContent = GetMacroContentFromCache(macro); + + // macroContent.IsEmpty may be true, meaning the macro produces no output, + // but still can be cached because its execution did not trigger any error. + // so we need to actually render, only if macroContent is null + if (macroContent != null) + { + return macroContent; + } + + // this will take care of errors + // it may throw, if we actually want to throw, so better not + // catch anything here and let the exception be thrown + Attempt attempt = ExecuteMacroOfType(macro, content); + + // by convention ExecuteMacroByType must either throw or return a result + // just check to avoid internal errors + macroContent = attempt.Result; + if (macroContent == null) + { + throw new Exception("Internal error, ExecuteMacroOfType returned no content."); + } + + // add to cache if render is successful + // content may be empty but that's not an issue + if (attempt.Success) + { + // write to cache (if appropriate) + await AddMacroContentToCacheAsync(macro, macroContent); + } + + return macroContent; + } + } + + /// + /// Executes a macro of a given type. + /// + private Attempt ExecuteMacroWithErrorWrapper(MacroModel macro, string msgIn, string msgOut, Func getMacroContent, Func msgErr) + { + using (_profilingLogger.DebugDuration(msgIn, msgOut)) + { + return ExecuteProfileMacroWithErrorWrapper(macro, msgIn, getMacroContent, msgErr); + } + } + + /// + /// Executes a macro of a given type. + /// + private Attempt ExecuteProfileMacroWithErrorWrapper(MacroModel macro, string msgIn, Func getMacroContent, Func msgErr) + { + try + { + return Attempt.Succeed(getMacroContent()); + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed {MsgIn}", msgIn); + + var macroErrorEventArgs = new MacroErrorEventArgs + { + Name = macro.Name, + Alias = macro.Alias, + MacroSource = macro.MacroSource, + Exception = e, + Behaviour = _contentSettings.MacroErrors, + }; + + switch (macroErrorEventArgs.Behaviour) + { + case MacroErrorBehaviour.Inline: + // do not throw, eat the exception, display the trace error message + return Attempt.Fail(new MacroContent { Text = msgErr() }, e); + case MacroErrorBehaviour.Silent: + // do not throw, eat the exception, do not display anything + return Attempt.Fail(new MacroContent { Text = string.Empty }, e); + case MacroErrorBehaviour.Content: + // do not throw, eat the exception, display the custom content + return Attempt.Fail(new MacroContent { Text = macroErrorEventArgs.Html ?? string.Empty }, e); + + // case MacroErrorBehaviour.Throw: + default: + // see http://issues.umbraco.org/issue/U4-497 at the end + // throw the original exception + throw; + } + } + } + + /// + /// Executes a macro. + /// + /// + /// Returns an attempt that is successful if the macro ran successfully. If the macro failed + /// to run properly, the attempt fails, though it may contain a content. But for instance that content + /// should not be cached. In that case the attempt may also contain an exception. + /// + private Attempt ExecuteMacroOfType(MacroModel model, IPublishedContent? content) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + // ensure that we are running against a published node (ie available in XML) + // that may not be the case if the macro is embedded in a RTE of an unpublished document + if (content == null) + { + return Attempt.Fail(new MacroContent { Text = "[macro failed (no content)]" }); + } + + return ExecuteMacroWithErrorWrapper( + model, + $"Executing PartialView: MacroSource=\"{model.MacroSource}\".", + "Executed PartialView.", + () => _partialViewMacroEngine.Execute(model, content), + () => _textService.Localize("errors", "macroErrorLoadingPartialView", new[] { model.MacroSource })); + } + + #endregion } diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs index 072ebf78a3..6c3b316841 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text.Encodings.Web; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -12,95 +8,91 @@ using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Extensions; -using static Umbraco.Cms.Core.Constants.Web.Routing; -namespace Umbraco.Cms.Web.Common.Macros +namespace Umbraco.Cms.Web.Common.Macros; + +/// +/// A macro engine using MVC Partial Views to execute. +/// +public class PartialViewMacroEngine { - /// - /// A macro engine using MVC Partial Views to execute. - /// - public class PartialViewMacroEngine + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; + + public PartialViewMacroEngine( + IHttpContextAccessor httpContextAccessor, + IModelMetadataProvider modelMetadataProvider, + ITempDataDictionaryFactory tempDataDictionaryFactory) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IModelMetadataProvider _modelMetadataProvider; - private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; - - public PartialViewMacroEngine( - IHttpContextAccessor httpContextAccessor, - IModelMetadataProvider modelMetadataProvider, - ITempDataDictionaryFactory tempDataDictionaryFactory) - { - _httpContextAccessor = httpContextAccessor; - _modelMetadataProvider = modelMetadataProvider; - _tempDataDictionaryFactory = tempDataDictionaryFactory; - } - - public MacroContent Execute(MacroModel macro, IPublishedContent content) - { - if (macro == null) - { - throw new ArgumentNullException(nameof(macro)); - } - - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (string.IsNullOrWhiteSpace(macro.MacroSource)) - { - throw new ArgumentException("The MacroSource property of the macro object cannot be null or empty"); - } - - HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); - - RouteData currentRouteData = httpContext.GetRouteData(); - - // Check if there's proxied ViewData (i.e. returned from a SurfaceController) - ProxyViewDataFeature? proxyViewDataFeature = httpContext.Features.Get(); - ViewDataDictionary viewData = proxyViewDataFeature?.ViewData ?? new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()); - ITempDataDictionary tempData = proxyViewDataFeature?.TempData ?? _tempDataDictionaryFactory.GetTempData(httpContext); - - var viewContext = new ViewContext( - new ActionContext(httpContext, currentRouteData, new ControllerActionDescriptor()), - new FakeView(), - viewData, - tempData, - TextWriter.Null, - new HtmlHelperOptions() - ); - - var writer = new StringWriter(); - var viewComponentContext = new ViewComponentContext( - new ViewComponentDescriptor(), - new Dictionary(), - HtmlEncoder.Default, - viewContext, - writer); - - var viewComponent = new PartialViewMacroViewComponent(macro, content, viewComponentContext); - - viewComponent.Invoke().Execute(viewComponentContext); - - var output = writer.GetStringBuilder().ToString(); - - return new MacroContent { Text = output }; - } - - private class FakeView : IView - { - /// - public Task RenderAsync(ViewContext context) => Task.CompletedTask; - - /// - public string Path { get; } = "View"; - } + _httpContextAccessor = httpContextAccessor; + _modelMetadataProvider = modelMetadataProvider; + _tempDataDictionaryFactory = tempDataDictionaryFactory; } + public MacroContent Execute(MacroModel macro, IPublishedContent content) + { + if (macro == null) + { + throw new ArgumentNullException(nameof(macro)); + } + + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (string.IsNullOrWhiteSpace(macro.MacroSource)) + { + throw new ArgumentException("The MacroSource property of the macro object cannot be null or empty"); + } + + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + + RouteData currentRouteData = httpContext.GetRouteData(); + + // Check if there's proxied ViewData (i.e. returned from a SurfaceController) + ProxyViewDataFeature? proxyViewDataFeature = httpContext.Features.Get(); + ViewDataDictionary viewData = proxyViewDataFeature?.ViewData ?? + new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()); + ITempDataDictionary tempData = + proxyViewDataFeature?.TempData ?? _tempDataDictionaryFactory.GetTempData(httpContext); + + var viewContext = new ViewContext( + new ActionContext(httpContext, currentRouteData, new ControllerActionDescriptor()), + new FakeView(), + viewData, + tempData, + TextWriter.Null, + new HtmlHelperOptions()); + + var writer = new StringWriter(); + var viewComponentContext = new ViewComponentContext( + new ViewComponentDescriptor(), + new Dictionary(), + HtmlEncoder.Default, + viewContext, + writer); + + var viewComponent = new PartialViewMacroViewComponent(macro, content, viewComponentContext); + + viewComponent.Invoke().Execute(viewComponentContext); + + var output = writer.GetStringBuilder().ToString(); + + return new MacroContent { Text = output }; + } + + private class FakeView : IView + { + /// + public string Path { get; } = "View"; + + /// + public Task RenderAsync(ViewContext context) => Task.CompletedTask; + } } diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs index 84b9e89051..5e8c22f762 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroPage.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Views; -namespace Umbraco.Cms.Web.Common.Macros +namespace Umbraco.Cms.Web.Common.Macros; + +/// +/// The base view class that PartialViewMacro views need to inherit from +/// +public abstract class PartialViewMacroPage : UmbracoViewPage { - /// - /// The base view class that PartialViewMacro views need to inherit from - /// - public abstract class PartialViewMacroPage : UmbracoViewPage - { } } diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs index 8afefc9d16..d0e3b7dbd7 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; using Umbraco.Cms.Core.Composing; @@ -7,44 +5,42 @@ using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Web.Common.Macros +namespace Umbraco.Cms.Web.Common.Macros; + +/// +/// Controller to render macro content for Partial View Macros +/// +// [MergeParentContextViewData] // TODO is this requeired now it is a ViewComponent? +[HideFromTypeFinder] // explicitly used: do *not* find and register it! +internal class PartialViewMacroViewComponent : ViewComponent { - /// - /// Controller to render macro content for Partial View Macros - /// - //[MergeParentContextViewData] // TODO is this requeired now it is a ViewComponent? - [HideFromTypeFinder] // explicitly used: do *not* find and register it! - internal class PartialViewMacroViewComponent : ViewComponent + private readonly IPublishedContent _content; + private readonly MacroModel _macro; + + public PartialViewMacroViewComponent( + MacroModel macro, + IPublishedContent content, + ViewComponentContext viewComponentContext) { - private readonly MacroModel _macro; - private readonly IPublishedContent _content; + _macro = macro; + _content = content; - public PartialViewMacroViewComponent( - MacroModel macro, - IPublishedContent content, - ViewComponentContext viewComponentContext) - { - _macro = macro; - _content = content; - // This must be set before Invoke is called else the call to View will end up - // using an empty ViewData instance because this hasn't been set yet. - ViewComponentContext = viewComponentContext; - } - - public IViewComponentResult Invoke() - { - var model = new PartialViewMacroModel( - _content, - _macro.Id, - _macro.Alias, - _macro.Name, - _macro.Properties.ToDictionary(x => x.Key, x => (object?)x.Value)); - - ViewViewComponentResult result = View(_macro.MacroSource, model); - - return result; - } + // This must be set before Invoke is called else the call to View will end up + // using an empty ViewData instance because this hasn't been set yet. + ViewComponentContext = viewComponentContext; } + public IViewComponentResult Invoke() + { + var model = new PartialViewMacroModel( + _content, + _macro.Id, + _macro.Alias, + _macro.Name, + _macro.Properties.ToDictionary(x => x.Key, x => (object?)x.Value)); + ViewViewComponentResult result = View(_macro.MacroSource, model); + + return result; + } } diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 1addc76abb..d91b6706c9 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; using SixLabors.ImageSharp; @@ -11,90 +8,94 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.ImageProcessors; using static Umbraco.Cms.Core.Models.ImageUrlGenerationOptions; -namespace Umbraco.Cms.Web.Common.Media +namespace Umbraco.Cms.Web.Common.Media; + +/// +/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp. +/// +/// +public class ImageSharpImageUrlGenerator : IImageUrlGenerator { /// - /// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp. + /// Initializes a new instance of the class. /// - /// - public class ImageSharpImageUrlGenerator : IImageUrlGenerator + /// The ImageSharp configuration. + public ImageSharpImageUrlGenerator(Configuration configuration) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) { - /// - public IEnumerable SupportedImageFileTypes { get; } + } - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) - { } + /// + /// Initializes a new instance of the class. + /// + /// The supported image file types/extensions. + /// + /// This constructor is only used for testing. + /// + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => + SupportedImageFileTypes = supportedImageFileTypes; - /// - /// Initializes a new instance of the class. - /// - /// The supported image file types/extensions. - /// - /// This constructor is only used for testing. - /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; + /// + public IEnumerable SupportedImageFileTypes { get; } - /// - public string? GetImageUrl(ImageUrlGenerationOptions options) + /// + public string? GetImageUrl(ImageUrlGenerationOptions? options) + { + if (options?.ImageUrl == null) { - if (options?.ImageUrl == null) - { - return null; - } - - var queryString = new Dictionary(); - - if (options.Crop is CropCoordinates crop) - { - queryString.Add(CropWebProcessor.Coordinates, FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}")); - } - - if (options.FocalPoint is FocalPointPosition focalPoint) - { - queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{focalPoint.Left},{focalPoint.Top}")); - } - - if (options.ImageCropMode is ImageCropMode imageCropMode) - { - queryString.Add(ResizeWebProcessor.Mode, imageCropMode.ToString().ToLowerInvariant()); - } - - if (options.ImageCropAnchor is ImageCropAnchor imageCropAnchor) - { - queryString.Add(ResizeWebProcessor.Anchor, imageCropAnchor.ToString().ToLowerInvariant()); - } - - if (options.Width is int width) - { - queryString.Add(ResizeWebProcessor.Width, width.ToString(CultureInfo.InvariantCulture)); - } - - if (options.Height is int height) - { - queryString.Add(ResizeWebProcessor.Height, height.ToString(CultureInfo.InvariantCulture)); - } - - if (options.Quality is int quality) - { - queryString.Add(QualityWebProcessor.Quality, quality.ToString(CultureInfo.InvariantCulture)); - } - - foreach (KeyValuePair kvp in QueryHelpers.ParseQuery(options.FurtherOptions)) - { - queryString.Add(kvp.Key, kvp.Value); - } - - if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrWhiteSpace(cacheBusterValue)) - { - queryString.Add("rnd", cacheBusterValue); - } - - return QueryHelpers.AddQueryString(options.ImageUrl, queryString); + return null; } + + var queryString = new Dictionary(); + + if (options.Crop is not null) + { + CropCoordinates? crop = options.Crop; + queryString.Add( + CropWebProcessor.Coordinates, + FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}")); + } + + if (options.FocalPoint is not null) + { + queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{options.FocalPoint.Left},{options.FocalPoint.Top}")); + } + + if (options.ImageCropMode is not null) + { + queryString.Add(ResizeWebProcessor.Mode, options.ImageCropMode.ToString()?.ToLowerInvariant()); + } + + if (options.ImageCropAnchor is not null) + { + queryString.Add(ResizeWebProcessor.Anchor, options.ImageCropAnchor.ToString()?.ToLowerInvariant()); + } + + if (options.Width is not null) + { + queryString.Add(ResizeWebProcessor.Width, options.Width?.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Height is not null) + { + queryString.Add(ResizeWebProcessor.Height, options.Height?.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Quality is not null) + { + queryString.Add(QualityWebProcessor.Quality, options.Quality?.ToString(CultureInfo.InvariantCulture)); + } + + foreach (KeyValuePair kvp in QueryHelpers.ParseQuery(options.FurtherOptions)) + { + queryString.Add(kvp.Key, kvp.Value); + } + + if (options.CacheBusterValue is not null && !string.IsNullOrWhiteSpace(options.CacheBusterValue)) + { + queryString.Add("rnd", options.CacheBusterValue); + } + + return QueryHelpers.AddQueryString(options.ImageUrl, queryString); } } diff --git a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs index afa837c7fe..ea2c0e02e4 100644 --- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs @@ -1,66 +1,78 @@ -using System.IO; using System.Text; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; +using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Web.Common.Middleware +namespace Umbraco.Cms.Web.Common.Middleware; + +/// +/// Executes when Umbraco booting fails in order to show the problem +/// +public class BootFailedMiddleware : IMiddleware { - /// - /// Executes when Umbraco booting fails in order to show the problem - /// - public class BootFailedMiddleware : IMiddleware + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IRuntimeState _runtimeState; + + public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment) + : this(runtimeState, hostingEnvironment, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IRuntimeState _runtimeState; - private readonly IHostingEnvironment _hostingEnvironment; + _runtimeState = runtimeState; + _hostingEnvironment = hostingEnvironment; + } - public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment) + public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment, IWebHostEnvironment webHostEnvironment) + { + _runtimeState = runtimeState; + _hostingEnvironment = hostingEnvironment; + _webHostEnvironment = webHostEnvironment; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + // TODO: It would be possible to redirect to the installer here in debug mode while + // still showing the error. This would be a lot more friendly than just the YSOD. + // We could also then have a different installer view for when package migrations fails + // and to retry each one individually. Perhaps this can happen in the future. + if (_runtimeState.Level == RuntimeLevel.BootFailed) { - _runtimeState = runtimeState; - _hostingEnvironment = hostingEnvironment; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - // TODO: It would be possible to redirect to the installer here in debug mode while - // still showing the error. This would be a lot more friendly than just the YSOD. - // We could also then have a different installer view for when package migrations fails - // and to retry each one individually. Perhaps this can happen in the future. - - if (_runtimeState.Level == RuntimeLevel.BootFailed) + // short circuit + if (_hostingEnvironment.IsDebugMode) { - // short circuit - - if (_hostingEnvironment.IsDebugMode) - { - BootFailedException.Rethrow(_runtimeState.BootFailedException); - } - else // Print a nice error page - { - context.Response.Clear(); - context.Response.StatusCode = 500; - - var file = GetBootErrorFileName(); - - var viewContent = await File.ReadAllTextAsync(file); - await context.Response.WriteAsync(viewContent, Encoding.UTF8); - } + BootFailedException.Rethrow(_runtimeState.BootFailedException); } else { - await next(context); + // Print a nice error page + context.Response.Clear(); + context.Response.StatusCode = 500; + + var file = GetBootErrorFileName(); + + var viewContent = await File.ReadAllTextAsync(file); + await context.Response.WriteAsync(viewContent, Encoding.UTF8); } - } - private string GetBootErrorFileName() + else { - var fileName = _hostingEnvironment.MapPathWebRoot("~/config/errors/BootFailed.html"); - if (File.Exists(fileName)) return fileName; - - return _hostingEnvironment.MapPathWebRoot("~/umbraco/views/errors/BootFailed.html"); + await next(context); } } + + private string GetBootErrorFileName() + { + var fileName = _webHostEnvironment.MapPathWebRoot("~/config/errors/BootFailed.html"); + if (File.Exists(fileName)) + { + return fileName; + } + + return _webHostEnvironment.MapPathWebRoot("~/umbraco/views/errors/BootFailed.html"); + } } diff --git a/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs index 84b4991b96..55001ca28c 100644 --- a/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs @@ -1,5 +1,5 @@ -using System; -using System.Threading.Tasks; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -7,72 +7,73 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Middleware +namespace Umbraco.Cms.Web.Common.Middleware; + +/// +/// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the +/// principal alongside any default authentication that takes place +/// +public class PreviewAuthenticationMiddleware : IMiddleware { - /// - /// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the principal alongside any default authentication that takes place - /// - public class PreviewAuthenticationMiddleware : IMiddleware + private readonly ILogger _logger; + + public PreviewAuthenticationMiddleware(ILogger logger) => _logger = logger; + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - private readonly ILogger _logger; + HttpRequest request = context.Request; - public PreviewAuthenticationMiddleware(ILogger logger) => _logger = logger; - - /// - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + // do not process if client-side request + if (request.IsClientSideRequest()) { - var request = context.Request; + await next(context); + return; + } - // do not process if client-side request - if (request.IsClientSideRequest()) + try + { + var isPreview = request.HasPreviewCookie() + && !request.IsBackOfficeRequest(); + + if (isPreview) { - await next(context); - return; - } + CookieAuthenticationOptions? cookieOptions = context.RequestServices + .GetRequiredService>() + .Get(Core.Constants.Security.BackOfficeAuthenticationType); - try - { - var isPreview = request.HasPreviewCookie() - && context.User != null - && !request.IsBackOfficeRequest(); - - if (isPreview) + if (cookieOptions == null) { - var cookieOptions = context.RequestServices.GetRequiredService>() - .Get(Core.Constants.Security.BackOfficeAuthenticationType); + throw new InvalidOperationException("No cookie options found with name " + + Core.Constants.Security.BackOfficeAuthenticationType); + } - if (cookieOptions == null) + // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. + // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication + // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. + if (cookieOptions.Cookie.Name is not null && + request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) + { + AuthenticationTicket? unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie); + ClaimsIdentity? backOfficeIdentity = unprotected?.Principal.GetUmbracoIdentity(); + if (backOfficeIdentity != null) { - throw new InvalidOperationException("No cookie options found with name " + Core.Constants.Security.BackOfficeAuthenticationType); + // Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Principal, this means we'll have 2 identities assigned to the principal which we can + // use to authorize the preview and allow for a back office User. + context.User.AddIdentity(backOfficeIdentity); } - - // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. - // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication - // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. - if (cookieOptions.Cookie.Name is not null && request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) - { - var unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie); - var backOfficeIdentity = unprotected?.Principal.GetUmbracoIdentity(); - if (backOfficeIdentity != null) - { - // Ok, we've got a real ticket, now we can add this ticket's identity to the current - // Principal, this means we'll have 2 identities assigned to the principal which we can - // use to authorize the preview and allow for a back office User. - context.User?.AddIdentity(backOfficeIdentity); - } - } - } } - catch (Exception ex) - { - // log any errors and continue the request without preview - _logger.LogError($"Unable to perform preview authentication: {ex.Message}"); - } - finally - { - await next(context); - } + } + catch (Exception ex) + { + // log any errors and continue the request without preview + _logger.LogError($"Unable to perform preview authentication: {ex.Message}"); + } + finally + { + await next(context); } } } diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs index 80e67c5857..b20c37099b 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs @@ -1,48 +1,45 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Serilog.Context; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Middleware -{ - /// - /// Adds request based serilog enrichers to the LogContext for each request - /// - public class UmbracoRequestLoggingMiddleware : IMiddleware - { - private readonly HttpSessionIdEnricher _sessionIdEnricher; - private readonly HttpRequestNumberEnricher _requestNumberEnricher; - private readonly HttpRequestIdEnricher _requestIdEnricher; +namespace Umbraco.Cms.Web.Common.Middleware; - public UmbracoRequestLoggingMiddleware( - HttpSessionIdEnricher sessionIdEnricher, - HttpRequestNumberEnricher requestNumberEnricher, - HttpRequestIdEnricher requestIdEnricher) +/// +/// Adds request based serilog enrichers to the LogContext for each request +/// +public class UmbracoRequestLoggingMiddleware : IMiddleware +{ + private readonly HttpRequestIdEnricher _requestIdEnricher; + private readonly HttpRequestNumberEnricher _requestNumberEnricher; + private readonly HttpSessionIdEnricher _sessionIdEnricher; + + public UmbracoRequestLoggingMiddleware( + HttpSessionIdEnricher sessionIdEnricher, + HttpRequestNumberEnricher requestNumberEnricher, + HttpRequestIdEnricher requestIdEnricher) + { + _sessionIdEnricher = sessionIdEnricher; + _requestNumberEnricher = requestNumberEnricher; + _requestIdEnricher = requestIdEnricher; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + // do not process if client-side request + if (context.Request.IsClientSideRequest()) { - _sessionIdEnricher = sessionIdEnricher; - _requestNumberEnricher = requestNumberEnricher; - _requestIdEnricher = requestIdEnricher; + await next(context); + return; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + // TODO: Need to decide if we want this stuff still, there's new request logging in serilog: + // https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this? + using (LogContext.Push(_sessionIdEnricher)) + using (LogContext.Push(_requestNumberEnricher)) + using (LogContext.Push(_requestIdEnricher)) { - // do not process if client-side request - if (context.Request.IsClientSideRequest()) - { - await next(context); - return; - } - - // TODO: Need to decide if we want this stuff still, there's new request logging in serilog: - // https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this? - - using (LogContext.Push(_sessionIdEnricher)) - using (LogContext.Push(_requestNumberEnricher)) - using (LogContext.Push(_requestIdEnricher)) - { - await next(context); - } + await next(context); } } } diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 3f4893e8dd..cf73ba481c 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -19,246 +16,249 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Profiler; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Middleware +namespace Umbraco.Cms.Web.Common.Middleware; + +/// +/// Manages Umbraco request objects and their lifetime +/// +/// +/// +/// This is responsible for initializing the content cache +/// +/// +/// This is responsible for creating and assigning an +/// +/// +public class UmbracoRequestMiddleware : IMiddleware { + private readonly BackOfficeWebAssets _backOfficeWebAssets; + private readonly IDefaultCultureAccessor _defaultCultureAccessor; + private readonly IEventAggregator _eventAggregator; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly WebProfiler? _profiler; + private readonly IRequestCache _requestCache; + private readonly IRuntimeState _runtimeState; + + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly IOptions _umbracoRequestOptions; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IVariationContextAccessor _variationContextAccessor; + private SmidgeOptions _smidgeOptions; /// - /// Manages Umbraco request objects and their lifetime + /// Initializes a new instance of the class. /// - /// - /// - /// This is responsible for initializing the content cache - /// - /// - /// This is responsible for creating and assigning an - /// - /// - public class UmbracoRequestMiddleware : IMiddleware + // Obsolete, scheduled for removal in V11 + [Obsolete("Use constructor that takes an IOptions")] + public UmbracoRequestMiddleware( + ILogger logger, + IUmbracoContextFactory umbracoContextFactory, + IRequestCache requestCache, + IEventAggregator eventAggregator, + IProfiler profiler, + IHostingEnvironment hostingEnvironment, + UmbracoRequestPaths umbracoRequestPaths, + BackOfficeWebAssets backOfficeWebAssets, + IOptionsMonitor smidgeOptions, + IRuntimeState runtimeState, + IVariationContextAccessor variationContextAccessor, + IDefaultCultureAccessor defaultCultureAccessor) + : this( + logger, + umbracoContextFactory, + requestCache, + eventAggregator, + profiler, + hostingEnvironment, + umbracoRequestPaths, + backOfficeWebAssets, + smidgeOptions, + runtimeState, + variationContextAccessor, + defaultCultureAccessor, + StaticServiceProvider.Instance.GetRequiredService>()) { - private readonly ILogger _logger; + } - private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly IRequestCache _requestCache; - private readonly IEventAggregator _eventAggregator; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly UmbracoRequestPaths _umbracoRequestPaths; - private readonly BackOfficeWebAssets _backOfficeWebAssets; - private readonly IRuntimeState _runtimeState; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IDefaultCultureAccessor _defaultCultureAccessor; - private readonly IOptions _umbracoRequestOptions; - private SmidgeOptions _smidgeOptions; - private readonly WebProfiler? _profiler; + /// + /// Initializes a new instance of the class. + /// + public UmbracoRequestMiddleware( + ILogger logger, + IUmbracoContextFactory umbracoContextFactory, + IRequestCache requestCache, + IEventAggregator eventAggregator, + IProfiler profiler, + IHostingEnvironment hostingEnvironment, + UmbracoRequestPaths umbracoRequestPaths, + BackOfficeWebAssets backOfficeWebAssets, + IOptionsMonitor smidgeOptions, + IRuntimeState runtimeState, + IVariationContextAccessor variationContextAccessor, + IDefaultCultureAccessor defaultCultureAccessor, + IOptions umbracoRequestOptions) + { + _logger = logger; + _umbracoContextFactory = umbracoContextFactory; + _requestCache = requestCache; + _eventAggregator = eventAggregator; + _hostingEnvironment = hostingEnvironment; + _umbracoRequestPaths = umbracoRequestPaths; + _backOfficeWebAssets = backOfficeWebAssets; + _runtimeState = runtimeState; + _variationContextAccessor = variationContextAccessor; + _defaultCultureAccessor = defaultCultureAccessor; + _umbracoRequestOptions = umbracoRequestOptions; + _smidgeOptions = smidgeOptions.CurrentValue; + _profiler = profiler as WebProfiler; // Ignore if not a WebProfiler -#pragma warning disable IDE0044 // Add readonly modifier - private static bool s_firstBackOfficeRequest; - private static bool s_firstBackOfficeReqestFlag; - private static object s_firstBackOfficeRequestLocker = new object(); -#pragma warning restore IDE0044 // Add readonly modifier + smidgeOptions.OnChange(x => _smidgeOptions = x); + } - /// - /// Initializes a new instance of the class. - /// - // Obsolete, scheduled for removal in V11 - [Obsolete("Use constructor that takes an IOptions")] - public UmbracoRequestMiddleware( - ILogger logger, - IUmbracoContextFactory umbracoContextFactory, - IRequestCache requestCache, - IEventAggregator eventAggregator, - IProfiler profiler, - IHostingEnvironment hostingEnvironment, - UmbracoRequestPaths umbracoRequestPaths, - BackOfficeWebAssets backOfficeWebAssets, - IOptionsMonitor smidgeOptions, - IRuntimeState runtimeState, - IVariationContextAccessor variationContextAccessor, - IDefaultCultureAccessor defaultCultureAccessor) - : this( - logger, - umbracoContextFactory, - requestCache, - eventAggregator, - profiler, - hostingEnvironment, - umbracoRequestPaths, - backOfficeWebAssets, - smidgeOptions, - runtimeState, - variationContextAccessor, - defaultCultureAccessor, - StaticServiceProvider.Instance.GetRequiredService>()) + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + // do not process if client-side request + if (context.Request.IsClientSideRequest() && + !_umbracoRequestOptions.Value.HandleAsServerSideRequest(context.Request)) { + // we need this here because for bundle requests, these are 'client side' requests that we need to handle + LazyInitializeBackOfficeServices(context.Request.Path); + await next(context); + return; } - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestMiddleware( - ILogger logger, - IUmbracoContextFactory umbracoContextFactory, - IRequestCache requestCache, - IEventAggregator eventAggregator, - IProfiler profiler, - IHostingEnvironment hostingEnvironment, - UmbracoRequestPaths umbracoRequestPaths, - BackOfficeWebAssets backOfficeWebAssets, - IOptionsMonitor smidgeOptions, - IRuntimeState runtimeState, - IVariationContextAccessor variationContextAccessor, - IDefaultCultureAccessor defaultCultureAccessor, - IOptions umbracoRequestOptions) + // Profiling start needs to be one of the first things that happens. + // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await + _profiler?.UmbracoApplicationBeginRequest(context, _runtimeState.Level); + + _variationContextAccessor.VariationContext ??= new VariationContext(_defaultCultureAccessor.DefaultCulture); + UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + + Uri? currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request); + _hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl); + + var pathAndQuery = context.Request.GetEncodedPathAndQuery(); + + try { - _logger = logger; - _umbracoContextFactory = umbracoContextFactory; - _requestCache = requestCache; - _eventAggregator = eventAggregator; - _hostingEnvironment = hostingEnvironment; - _umbracoRequestPaths = umbracoRequestPaths; - _backOfficeWebAssets = backOfficeWebAssets; - _runtimeState = runtimeState; - _variationContextAccessor = variationContextAccessor; - _defaultCultureAccessor = defaultCultureAccessor; - _umbracoRequestOptions = umbracoRequestOptions; - _smidgeOptions = smidgeOptions.CurrentValue; - _profiler = profiler as WebProfiler; // Ignore if not a WebProfiler - - smidgeOptions.OnChange(x => _smidgeOptions = x); - } - - /// - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - // do not process if client-side request - if (context.Request.IsClientSideRequest() && !_umbracoRequestOptions.Value.HandleAsServerSideRequest(context.Request)) - { - // we need this here because for bundle requests, these are 'client side' requests that we need to handle - LazyInitializeBackOfficeServices(context.Request.Path); - await next(context); - return; - } - - // Profiling start needs to be one of the first things that happens. - // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await - _profiler?.UmbracoApplicationBeginRequest(context, _runtimeState.Level); - - _variationContextAccessor.VariationContext ??= new VariationContext(_defaultCultureAccessor.DefaultCulture); - UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); - - Uri? currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request); - _hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl); - - var pathAndQuery = context.Request.GetEncodedPathAndQuery(); + // Verbose log start of every request + LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); + _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, pathAndQuery); try { - // Verbose log start of every request - LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); - _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, pathAndQuery); - - try - { - LazyInitializeBackOfficeServices(context.Request.Path); - await _eventAggregator.PublishAsync(new UmbracoRequestBeginNotification(umbracoContextReference.UmbracoContext)); - } - catch (Exception ex) - { - // try catch so we don't kill everything in all requests - _logger.LogError(ex.Message); - } - finally - { - try - { - await next(context); - - } - finally - { - await _eventAggregator.PublishAsync(new UmbracoRequestEndNotification(umbracoContextReference.UmbracoContext)); - } - } + LazyInitializeBackOfficeServices(context.Request.Path); + await _eventAggregator.PublishAsync( + new UmbracoRequestBeginNotification(umbracoContextReference.UmbracoContext)); + } + catch (Exception ex) + { + // try catch so we don't kill everything in all requests + _logger.LogError(ex.Message); } finally { - // Verbose log end of every request (in v8 we didn't log the end request of ALL requests, only the front-end which was - // strange since we always logged the beginning, so now we just log start/end of all requests) - LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); - _logger.LogTrace("End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds); - try { - DisposeHttpContextItems(context.Request); + await next(context); } finally { - // Dispose the umbraco context reference which will in turn dispose the UmbracoContext itself. - umbracoContextReference.Dispose(); + await _eventAggregator.PublishAsync( + new UmbracoRequestEndNotification(umbracoContextReference.UmbracoContext)); } } + } + finally + { + // Verbose log end of every request (in v8 we didn't log the end request of ALL requests, only the front-end which was + // strange since we always logged the beginning, so now we just log start/end of all requests) + LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); + _logger.LogTrace( + "End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", + httpRequestId, + pathAndQuery, + DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds); - // Profiling end needs to be last of the first things that happens. - // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await - _profiler?.UmbracoApplicationEndRequest(context, _runtimeState.Level); + try + { + DisposeHttpContextItems(context.Request); + } + finally + { + // Dispose the umbraco context reference which will in turn dispose the UmbracoContext itself. + umbracoContextReference.Dispose(); + } } - /// - /// Used to lazily initialize any back office services when the first request to the back office is made - /// - /// - /// - private void LazyInitializeBackOfficeServices(PathString absPath) - { - if (s_firstBackOfficeRequest) - { - return; - } + // Profiling end needs to be last of the first things that happens. + // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await + _profiler?.UmbracoApplicationEndRequest(context, _runtimeState.Level); + } - if (_umbracoRequestPaths.IsBackOfficeRequest(absPath) - || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.CompositeFilePath}") ?? false) - || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}") ?? false)) - { - LazyInitializer.EnsureInitialized(ref s_firstBackOfficeRequest, ref s_firstBackOfficeReqestFlag, ref s_firstBackOfficeRequestLocker, () => + /// + /// Used to lazily initialize any back office services when the first request to the back office is made + /// + private void LazyInitializeBackOfficeServices(PathString absPath) + { + if (s_firstBackOfficeRequest) + { + return; + } + + if (_umbracoRequestPaths.IsBackOfficeRequest(absPath) + || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.CompositeFilePath}") ?? false) + || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}") ?? false)) + { + LazyInitializer.EnsureInitialized(ref s_firstBackOfficeRequest, ref s_firstBackOfficeReqestFlag, + ref s_firstBackOfficeRequestLocker, () => { _backOfficeWebAssets.CreateBundles(); return true; }); - } - } - - private Uri? GetApplicationUrlFromCurrentRequest(HttpRequest request) - { - // We only consider GET and POST. - // Especially the DEBUG sent when debugging the application is annoying because it uses http, even when the https is available. - if (request.Method == "GET" || request.Method == "POST") - { - return new Uri($"{request.Scheme}://{request.Host}{request.PathBase}", UriKind.Absolute); - - } - return null; - } - - /// - /// Dispose some request scoped objects that we are maintaining the lifecycle for. - /// - private void DisposeHttpContextItems(HttpRequest request) - { - // do not process if client-side request - if (request.IsClientSideRequest()) - { - return; - } - - // ensure this is disposed by DI at the end of the request - IHttpScopeReference httpScopeReference = request.HttpContext.RequestServices.GetRequiredService(); - httpScopeReference.Register(); } } + + private Uri? GetApplicationUrlFromCurrentRequest(HttpRequest request) + { + // We only consider GET and POST. + // Especially the DEBUG sent when debugging the application is annoying because it uses http, even when the https is available. + if (request.Method == "GET" || request.Method == "POST") + { + return new Uri($"{request.Scheme}://{request.Host}{request.PathBase}", UriKind.Absolute); + } + + return null; + } + + /// + /// Dispose some request scoped objects that we are maintaining the lifecycle for. + /// + private void DisposeHttpContextItems(HttpRequest request) + { + // do not process if client-side request + if (request.IsClientSideRequest()) + { + return; + } + + // ensure this is disposed by DI at the end of the request + IHttpScopeReference httpScopeReference = + request.HttpContext.RequestServices.GetRequiredService(); + httpScopeReference.Register(); + } + +#pragma warning disable IDE0044 // Add readonly modifier + private static bool s_firstBackOfficeRequest; + private static bool s_firstBackOfficeReqestFlag; + private static object s_firstBackOfficeRequestLocker = new(); +#pragma warning restore IDE0044 // Add readonly modifier } diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs index 6cc20f4f52..fb946cbc27 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs @@ -1,6 +1,4 @@ -using System; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; @@ -10,162 +8,160 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.ModelBinders +namespace Umbraco.Cms.Web.Common.ModelBinders; + +/// +/// Maps view models, supporting mapping to and from any or +/// . +/// +public class ContentModelBinder : IModelBinder { + private readonly IEventAggregator _eventAggregator; /// - /// Maps view models, supporting mapping to and from any or . + /// Initializes a new instance of the class. /// - public class ContentModelBinder : IModelBinder + public ContentModelBinder(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; + + /// + public Task BindModelAsync(ModelBindingContext bindingContext) { - private readonly IEventAggregator _eventAggregator; - - /// - /// Initializes a new instance of the class. - /// - public ContentModelBinder(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; - - /// - public Task BindModelAsync(ModelBindingContext bindingContext) + // Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality + // only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder + // in the aspnet pipeline it will really only support converting from IPublishedContent which is contained + // in the UmbracoRouteValues --> IContentModel + UmbracoRouteValues? umbracoRouteValues = bindingContext.HttpContext.Features.Get(); + if (umbracoRouteValues is null) { - // Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality - // only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder - // in the aspnet pipeline it will really only support converting from IPublishedContent which is contained - // in the UmbracoRouteValues --> IContentModel - UmbracoRouteValues? umbracoRouteValues = bindingContext.HttpContext.Features.Get(); - if (umbracoRouteValues is null) - { - return Task.CompletedTask; - } - - BindModel(bindingContext, umbracoRouteValues.PublishedRequest.PublishedContent, bindingContext.ModelType); return Task.CompletedTask; } - // source is the model that we have - // modelType is the type of the model that we need to bind to - // - // create a model object of the modelType by mapping: - // { ContentModel, ContentModel, IPublishedContent } - // to - // { ContentModel, ContentModel, IPublishedContent } + BindModel(bindingContext, umbracoRouteValues.PublishedRequest.PublishedContent, bindingContext.ModelType); + return Task.CompletedTask; + } - /// - /// Attempts to bind the model - /// - public void BindModel(ModelBindingContext bindingContext, object? source, Type modelType) + // source is the model that we have + // modelType is the type of the model that we need to bind to + // + // create a model object of the modelType by mapping: + // { ContentModel, ContentModel, IPublishedContent } + // to + // { ContentModel, ContentModel, IPublishedContent } + + /// + /// Attempts to bind the model + /// + public void BindModel(ModelBindingContext bindingContext, object? source, Type modelType) + { + // Null model, return + if (source == null) { - // Null model, return - if (source == null) - { - return; - } - - // If types already match, return - Type sourceType = source.GetType(); - if (sourceType.Inherits(modelType)) - { - bindingContext.Result = ModelBindingResult.Success(source); - return; - } - - // Try to grab the content - var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent - if (sourceContent == null && sourceType.Implements()) - { - // else check if it's an IContentModel, and get the content - sourceContent = ((IContentModel)source).Content; - } - - if (sourceContent == null) - { - // else check if we can convert it to a content - Attempt attempt1 = source.TryConvertTo(); - if (attempt1.Success) - { - sourceContent = attempt1.Result; - } - } - - // If we have a content - if (sourceContent != null) - { - // If model is IPublishedContent, check content type and return - if (modelType.Implements()) - { - if (sourceContent.GetType().Inherits(modelType) == false) - { - ThrowModelBindingException(true, false, sourceContent.GetType(), modelType); - } - - bindingContext.Result = ModelBindingResult.Success(sourceContent); - return; - } - - // If model is ContentModel, create and return - if (modelType == typeof(ContentModel)) - { - bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent)); - return; - } - - // If model is ContentModel, check content type, then create and return - if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) - { - Type targetContentType = modelType.GetGenericArguments()[0]; - if (sourceContent.GetType().Inherits(targetContentType) == false) - { - ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType); - } - - bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent)); - return; - } - } - - // Last chance : try to convert - Attempt attempt2 = source.TryConvertTo(modelType); - if (attempt2.Success) - { - bindingContext.Result = ModelBindingResult.Success(attempt2.Result); - return; - } - - // Fail - ThrowModelBindingException(false, false, sourceType, modelType); return; } - private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType) + // If types already match, return + Type sourceType = source.GetType(); + if (sourceType.Inherits(modelType)) { - var msg = new StringBuilder(); - - // prepare message - msg.Append("Cannot bind source"); - if (sourceContent) - { - msg.Append(" content"); - } - - msg.Append(" type "); - msg.Append(sourceType.FullName); - msg.Append(" to model"); - if (modelContent) - { - msg.Append(" content"); - } - - msg.Append(" type "); - msg.Append(modelType.FullName); - msg.Append("."); - - // raise event, to give model factories a chance at reporting - // the error with more details, and optionally request that - // the application restarts. - var args = new ModelBindingErrorNotification(sourceType, modelType, msg); - _eventAggregator.Publish(args); - - throw new ModelBindingException(msg.ToString()); + bindingContext.Result = ModelBindingResult.Success(source); + return; } + + // Try to grab the content + var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent + if (sourceContent == null && sourceType.Implements()) + { + // else check if it's an IContentModel, and get the content + sourceContent = ((IContentModel)source).Content; + } + + if (sourceContent == null) + { + // else check if we can convert it to a content + Attempt attempt1 = source.TryConvertTo(); + if (attempt1.Success) + { + sourceContent = attempt1.Result; + } + } + + // If we have a content + if (sourceContent != null) + { + // If model is IPublishedContent, check content type and return + if (modelType.Implements()) + { + if (sourceContent.GetType().Inherits(modelType) == false) + { + ThrowModelBindingException(true, false, sourceContent.GetType(), modelType); + } + + bindingContext.Result = ModelBindingResult.Success(sourceContent); + return; + } + + // If model is ContentModel, create and return + if (modelType == typeof(ContentModel)) + { + bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent)); + return; + } + + // If model is ContentModel, check content type, then create and return + if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) + { + Type targetContentType = modelType.GetGenericArguments()[0]; + if (sourceContent.GetType().Inherits(targetContentType) == false) + { + ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType); + } + + bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent)); + return; + } + } + + // Last chance : try to convert + Attempt attempt2 = source.TryConvertTo(modelType); + if (attempt2.Success) + { + bindingContext.Result = ModelBindingResult.Success(attempt2.Result); + return; + } + + // Fail + ThrowModelBindingException(false, false, sourceType, modelType); + } + + private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType) + { + var msg = new StringBuilder(); + + // prepare message + msg.Append("Cannot bind source"); + if (sourceContent) + { + msg.Append(" content"); + } + + msg.Append(" type "); + msg.Append(sourceType.FullName); + msg.Append(" to model"); + if (modelContent) + { + msg.Append(" content"); + } + + msg.Append(" type "); + msg.Append(modelType.FullName); + msg.Append("."); + + // raise event, to give model factories a chance at reporting + // the error with more details, and optionally request that + // the application restarts. + var args = new ModelBindingErrorNotification(sourceType, modelType, msg); + _eventAggregator.Publish(args); + + throw new ModelBindingException(msg.ToString()); } } diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinderProvider.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinderProvider.cs index 5fe1638394..7ba9780d81 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinderProvider.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinderProvider.cs @@ -1,30 +1,30 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Web.Common.ModelBinders +namespace Umbraco.Cms.Web.Common.ModelBinders; + +/// +/// The provider for mapping view models, supporting mapping to and from any +/// IPublishedContent or IContentModel. +/// +public class ContentModelBinderProvider : IModelBinderProvider { - /// - /// The provider for mapping view models, supporting mapping to and from any IPublishedContent or IContentModel. - /// - public class ContentModelBinderProvider : IModelBinderProvider + public IModelBinder? GetBinder(ModelBinderProviderContext context) { - public IModelBinder? GetBinder(ModelBinderProviderContext context) + Type modelType = context.Metadata.ModelType; + + // Can bind to ContentModel (exact type match) + // or to ContentModel (exact generic type match) + // or to TContent where TContent : IPublishedContent (any IPublishedContent implementation) + if (modelType == typeof(ContentModel) || + (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) || + typeof(IPublishedContent).IsAssignableFrom(modelType)) { - var modelType = context.Metadata.ModelType; - - // Can bind to ContentModel (exact type match) - // or to ContentModel (exact generic type match) - // or to TContent where TContent : IPublishedContent (any IPublishedContent implementation) - if (modelType == typeof(ContentModel) || - (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) || - typeof(IPublishedContent).IsAssignableFrom(modelType)) - { - return new BinderTypeModelBinder(typeof(ContentModelBinder)); - } - - return null; + return new BinderTypeModelBinder(typeof(ContentModelBinder)); } + + return null; } } diff --git a/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs index a8c09475d9..7f4841ef01 100644 --- a/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs @@ -1,50 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Primitives; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.ModelBinders -{ - /// - /// Allows an Action to execute with an arbitrary number of QueryStrings - /// - /// - /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number - /// but this will allow you to do it. - /// - public sealed class HttpQueryStringModelBinder : IModelBinder - { - public Task BindModelAsync(ModelBindingContext bindingContext) - { - var queryStrings = GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query); - var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray(); - if (queryStringKeys.InvariantContains("culture") == false) - { - queryStrings.Add("culture", new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture())); - } +namespace Umbraco.Cms.Web.Common.ModelBinders; - var formData = new FormCollection(queryStrings); - bindingContext.Result = ModelBindingResult.Success(formData); - return Task.CompletedTask; +/// +/// Allows an Action to execute with an arbitrary number of QueryStrings +/// +/// +/// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number +/// but this will allow you to do it. +/// +public sealed class HttpQueryStringModelBinder : IModelBinder +{ + public Task BindModelAsync(ModelBindingContext bindingContext) + { + Dictionary queryStrings = + GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query); + var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray(); + if (queryStringKeys.InvariantContains("culture") == false) + { + queryStrings.Add( + "culture", + new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture())); } - private Dictionary GetQueryAsDictionary(IQueryCollection query) + var formData = new FormCollection(queryStrings); + bindingContext.Result = ModelBindingResult.Success(formData); + return Task.CompletedTask; + } + + private Dictionary GetQueryAsDictionary(IQueryCollection? query) + { + var result = new Dictionary(); + if (query == null) { - var result = new Dictionary(); - if (query == null) - { - return result; - } - - foreach (var item in query) - { - result.Add(item.Key, item.Value); - } - return result; } + + foreach (KeyValuePair item in query) + { + result.Add(item.Key, item.Value); + } + + return result; } } diff --git a/src/Umbraco.Web.Common/ModelBinders/ModelBindingException.cs b/src/Umbraco.Web.Common/ModelBinders/ModelBindingException.cs index d8418b17a6..84d95682f0 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ModelBindingException.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ModelBindingException.cs @@ -1,46 +1,57 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Web.Common.ModelBinders +namespace Umbraco.Cms.Web.Common.ModelBinders; + +/// +/// The exception that is thrown when an error occurs while binding a source to a model. +/// +/// +/// Migrated to .NET Core +[Serializable] +public class ModelBindingException : Exception { /// - /// The exception that is thrown when an error occurs while binding a source to a model. + /// Initializes a new instance of the class. /// - /// - /// Migrated to .NET Core - [Serializable] - public class ModelBindingException : Exception + public ModelBindingException() { - /// - /// Initializes a new instance of the class. - /// - public ModelBindingException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ModelBindingException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ModelBindingException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public ModelBindingException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public ModelBindingException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected ModelBindingException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected ModelBindingException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Web.Common/ModelBinders/UmbracoJsonModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/UmbracoJsonModelBinder.cs index e681785f20..6372930898 100644 --- a/src/Umbraco.Web.Common/ModelBinders/UmbracoJsonModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/UmbracoJsonModelBinder.cs @@ -2,45 +2,46 @@ using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; +using Newtonsoft.Json; using Umbraco.Cms.Web.Common.Formatters; -namespace Umbraco.Cms.Web.Common.ModelBinders +namespace Umbraco.Cms.Web.Common.ModelBinders; + +/// +/// A custom body model binder that only uses a to bind body action +/// parameters +/// +public class UmbracoJsonModelBinder : BodyModelBinder { - /// - /// A custom body model binder that only uses a to bind body action parameters - /// - public class UmbracoJsonModelBinder : BodyModelBinder, IModelBinder + public UmbracoJsonModelBinder( + ArrayPool arrayPool, + ObjectPoolProvider objectPoolProvider, + IHttpRequestStreamReaderFactory readerFactory, + ILoggerFactory loggerFactory) + : base(GetNewtonsoftJsonFormatter(loggerFactory, arrayPool, objectPoolProvider), readerFactory, loggerFactory) { - public UmbracoJsonModelBinder(ArrayPool arrayPool, ObjectPoolProvider objectPoolProvider, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory) - : base(GetNewtonsoftJsonFormatter(loggerFactory, arrayPool, objectPoolProvider), readerFactory, loggerFactory) + } + + private static IInputFormatter[] GetNewtonsoftJsonFormatter(ILoggerFactory logger, ArrayPool arrayPool, ObjectPoolProvider objectPoolProvider) + { + var jsonOptions = new MvcNewtonsoftJsonOptions { AllowInputFormatterExceptionMessages = true }; + + JsonSerializerSettings ss = jsonOptions.SerializerSettings; // Just use the defaults as base + + // We need to ignore required attributes when serializing. E.g UserSave.ChangePassword. Otherwise the model is not model bound. + ss.ContractResolver = new IgnoreRequiredAttributesResolver(); + return new IInputFormatter[] { - } - - private static IInputFormatter[] GetNewtonsoftJsonFormatter(ILoggerFactory logger, ArrayPool arrayPool, ObjectPoolProvider objectPoolProvider) - { - var jsonOptions = new MvcNewtonsoftJsonOptions - { - AllowInputFormatterExceptionMessages = true - }; - - var ss = jsonOptions.SerializerSettings; // Just use the defaults as base - - // We need to ignore required attributes when serializing. E.g UserSave.ChangePassword. Otherwise the model is not model bound. - ss.ContractResolver = new IgnoreRequiredAttributesResolver(); - return new IInputFormatter[] - { - new NewtonsoftJsonInputFormatter( - logger.CreateLogger(), - jsonOptions.SerializerSettings, // Just use the defaults - arrayPool, - objectPoolProvider, - new MvcOptions(), // The only option that NewtonsoftJsonInputFormatter uses is SuppressInputFormatterBuffering - jsonOptions) - }; - } + new NewtonsoftJsonInputFormatter( + logger.CreateLogger(), + jsonOptions.SerializerSettings, // Just use the defaults + arrayPool, + objectPoolProvider, + new MvcOptions(), // The only option that NewtonsoftJsonInputFormatter uses is SuppressInputFormatterBuffering + jsonOptions), + }; } } diff --git a/src/Umbraco.Web.Common/Models/LoginModel.cs b/src/Umbraco.Web.Common/Models/LoginModel.cs index 304953a925..d946fa1c6b 100644 --- a/src/Umbraco.Web.Common/Models/LoginModel.cs +++ b/src/Umbraco.Web.Common/Models/LoginModel.cs @@ -1,21 +1,19 @@ using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; -namespace Umbraco.Cms.Web.Common.Models +namespace Umbraco.Cms.Web.Common.Models; + +public class LoginModel : PostRedirectModel { - public class LoginModel : PostRedirectModel - { - [Required] - [Display(Name = "User name")] - public string Username { get; set; } = null!; + [Required] + [Display(Name = "User name")] + public string Username { get; set; } = null!; - [Required] - [DataType(DataType.Password)] - [Display(Name = "Password")] - [StringLength(maximumLength: 256)] - public string Password { get; set; } = null!; + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + [StringLength(256)] + public string Password { get; set; } = null!; - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } - } + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } } diff --git a/src/Umbraco.Web.Common/Models/PostRedirectModel.cs b/src/Umbraco.Web.Common/Models/PostRedirectModel.cs index a9b929282d..fc10782d19 100644 --- a/src/Umbraco.Web.Common/Models/PostRedirectModel.cs +++ b/src/Umbraco.Web.Common/Models/PostRedirectModel.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Web.Common.Models +namespace Umbraco.Cms.Web.Common.Models; + +/// +/// A base model containing a value to indicate to Umbraco where to redirect to after Posting if +/// a developer doesn't want the controller to redirect to the current Umbraco page - which is the default. +/// +public class PostRedirectModel { /// - /// A base model containing a value to indicate to Umbraco where to redirect to after Posting if - /// a developer doesn't want the controller to redirect to the current Umbraco page - which is the default. + /// The path to redirect to when update is successful, if not specified then the user will be + /// redirected to the current Umbraco page /// - public class PostRedirectModel - { - /// - /// The path to redirect to when update is successful, if not specified then the user will be - /// redirected to the current Umbraco page - /// - public string? RedirectUrl { get; set; } - } + public string? RedirectUrl { get; set; } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs index a1aa5b24d8..7749c4cbc9 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs @@ -1,19 +1,14 @@ -using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.ModelsBuilder; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; -using Umbraco.Cms.Infrastructure.WebAssets; -using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Cms.Web.Common.ModelsBuilder; /* @@ -74,94 +69,89 @@ using Umbraco.Cms.Web.Common.ModelsBuilder; * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. */ -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for for the common Umbraco functionality +/// +public static class UmbracoBuilderDependencyInjectionExtensions { /// - /// Extension methods for for the common Umbraco functionality + /// Adds umbraco's embedded model builder support /// - public static class UmbracoBuilderDependencyInjectionExtensions + public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) { - /// - /// Adds umbraco's embedded model builder support - /// - public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) + var umbServices = + new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); + if (builder.Services.Contains(umbServices)) { - var umbServices = new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); - if (builder.Services.Contains(umbServices)) - { - // if this ext method is called more than once just exit - return builder; - } - - builder.Services.Add(umbServices); - - builder.AddInMemoryModelsRazorEngine(); - - // TODO: I feel like we could just do builder.AddNotificationHandler() and it - // would automatically just register for all implemented INotificationHandler{T}? - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - // This is what the community MB would replace, all of the above services are fine to be registered - // even if the community MB is in place. - builder.Services.AddSingleton(factory => - { - ModelsBuilderSettings config = factory.GetRequiredService>().Value; - if (config.ModelsMode == ModelsMode.InMemoryAuto) - { - return factory.GetRequiredService(); - } - else - { - return factory.CreateDefaultPublishedModelFactory(); - } - }); - - - if (!builder.Services.Any(x=>x.ServiceType == typeof(IModelsBuilderDashboardProvider))) - { - builder.Services.AddUnique(); - } - + // if this ext method is called more than once just exit return builder; } - private static IUmbracoBuilder AddInMemoryModelsRazorEngine(this IUmbracoBuilder builder) + builder.Services.Add(umbServices); + + builder.AddInMemoryModelsRazorEngine(); + + // TODO: I feel like we could just do builder.AddNotificationHandler() and it + // would automatically just register for all implemented INotificationHandler{T}? + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + // This is what the community MB would replace, all of the above services are fine to be registered + // even if the community MB is in place. + builder.Services.AddSingleton(factory => { - // See notes in RefreshingRazorViewEngine for information on what this is doing. - - // copy the current collection, we need to use this later to rebuild a container - // to re-create the razor compiler provider - var initialCollection = new ServiceCollection + ModelsBuilderSettings config = factory.GetRequiredService>().Value; + if (config.ModelsMode == ModelsMode.InMemoryAuto) { - builder.Services - }; + return factory.GetRequiredService(); + } - // Replace the default with our custom engine - builder.Services.AddSingleton( - s => new RefreshingRazorViewEngine( - () => - { - // re-create the original container so that a brand new IRazorPageActivator - // is produced, if we don't re-create the container then it will just return the same instance. - ServiceProvider recreatedServices = initialCollection.BuildServiceProvider(); - return recreatedServices.GetRequiredService(); - }, s.GetRequiredService())); + return factory.CreateDefaultPublishedModelFactory(); + }); - return builder; + if (!builder.Services.Any(x => x.ServiceType == typeof(IModelsBuilderDashboardProvider))) + { + builder.Services.AddUnique(); } + + return builder; + } + + private static IUmbracoBuilder AddInMemoryModelsRazorEngine(this IUmbracoBuilder builder) + { + // See notes in RefreshingRazorViewEngine for information on what this is doing. + + // copy the current collection, we need to use this later to rebuild a container + // to re-create the razor compiler provider + var initialCollection = new ServiceCollection { builder.Services }; + + // Replace the default with our custom engine + builder.Services.AddSingleton( + s => new RefreshingRazorViewEngine( + () => + { + // re-create the original container so that a brand new IRazorPageActivator + // is produced, if we don't re-create the container then it will just return the same instance. + ServiceProvider recreatedServices = initialCollection.BuildServiceProvider(); + return recreatedServices.GetRequiredService(); + }, + s.GetRequiredService())); + + return builder; } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs index 4e3efcc90c..51e2564102 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Umbraco.Cms.Web.Common.ModelsBuilder; -namespace Umbraco.Cms.Web.Common.ModelsBuilder +public interface IModelsBuilderDashboardProvider { - public interface IModelsBuilderDashboardProvider - { - string? GetUrl(); - } + string? GetUrl(); } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs index cd5272b500..d3b47f6fb8 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs @@ -22,30 +22,30 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder { internal class InMemoryModelFactory : IAutoPublishedModelFactory, IRegisteredObject, IDisposable { - private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; + private static readonly Regex s_usingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex s_aattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex s_assemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); + private static readonly string[] s_ourFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - private volatile bool _hasModels; // volatile 'cos reading outside lock - private bool _pendingRebuild; private readonly IProfilingLogger _profilingLogger; private readonly ILogger _logger; private readonly FileSystemWatcher? _watcher; - private int _ver; - private int? _skipver; - private readonly int _debugLevel; - private RoslynCompiler? _roslynCompiler; - private UmbracoAssemblyLoadContext? _currentAssemblyLoadContext; private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( - private static readonly Regex s_assemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); - private static readonly string[] s_ourFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; - private ModelsBuilderSettings _config; private readonly IHostingEnvironment _hostingEnvironment; private readonly IApplicationShutdownRegistry _hostingLifetime; private readonly ModelsGenerationError _errors; private readonly IPublishedValueFallback _publishedValueFallback; private readonly ApplicationPartManager _applicationPartManager; - private static readonly Regex s_usingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); - private static readonly Regex s_aattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); private readonly Lazy _pureLiveDirectory = null!; + private readonly int _debugLevel; + private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; + private volatile bool _hasModels; // volatile 'cos reading outside lock + private bool _pendingRebuild; + private int _ver; + private int? _skipver; + private RoslynCompiler? _roslynCompiler; + private UmbracoAssemblyLoadContext? _currentAssemblyLoadContext; + private ModelsBuilderSettings _config; private bool _disposedValue; public InMemoryModelFactory( @@ -99,8 +99,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder public event EventHandler? ModelsChanged; - private UmbracoServices UmbracoServices => _umbracoServices.Value; - /// /// Gets the currently loaded Live models assembly /// @@ -112,6 +110,8 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder /// public object SyncRoot { get; } = new object(); + private UmbracoServices UmbracoServices => _umbracoServices.Value; + /// /// Gets the RoslynCompiler /// @@ -147,7 +147,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder public IPublishedElement CreateModel(IPublishedElement element) { // get models, rebuilding them if needed - Dictionary? infos = EnsureModels()?.ModelInfos; + Dictionary? infos = EnsureModels().ModelInfos; if (infos == null) { return element; @@ -169,8 +169,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder Infos infos = EnsureModels(); // fail fast - if (infos is null || - alias is null || + if (alias is null || infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || modelInfo.ModelType is null) @@ -196,7 +195,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder Infos infos = EnsureModels(); // fail fast - if (infos is null || alias is null || infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo)) + if (alias is null || infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo)) { return new List(); } @@ -360,9 +359,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder } } - - public string PureLiveDirectoryAbsolute() => _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData+"/InMemoryAuto"); - + public string PureLiveDirectoryAbsolute() => _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData + "/InMemoryAuto"); // This is NOT thread safe but it is only called from within a lock private Assembly ReloadAssembly(string pathToAssembly) @@ -413,7 +410,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder // This is NOT thread safe but it is only called from within a lock private Assembly GetModelsAssembly(bool forceRebuild) { - if (!Directory.Exists(_pureLiveDirectory.Value)) { Directory.CreateDirectory(_pureLiveDirectory.Value); @@ -531,6 +527,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder // generate code, save var code = GenerateModelsCode(typeModels); + // add extra attributes, // IsLive=true helps identifying Assemblies that contain live models // AssemblyVersion is so that we have a different version for each rebuild @@ -544,7 +541,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder // generate proj, save var projFiles = new Dictionary { - { "models.generated.cs", code } + { "models.generated.cs", code }, }; var proj = GenerateModelsProj(projFiles); File.WriteAllText(projFile, proj); @@ -633,7 +630,9 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder File.SetLastWriteTime(projFile, DateTime.Now); } } - catch { /* enough */ } + catch + { /* enough */ + } } private static Infos RegisterModels(IEnumerable types) @@ -696,7 +695,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder private string GenerateModelsCode(IList typeModels) { - if (!Directory.Exists(_pureLiveDirectory.Value)) { Directory.CreateDirectory(_pureLiveDirectory.Value); @@ -716,8 +714,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder return code; } - - private static string GenerateModelsProj(IDictionary files) { // ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj @@ -785,11 +781,11 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder // race conditions can occur on slow Cloud filesystems and then we keep // rebuilding - //if (_building && OurFiles.Contains(changed)) - //{ + // if (_building && OurFiles.Contains(changed)) + // { // //_logger.LogInformation("Ignoring files self-changes."); // return; - //} + // } // always ignore our own file changes if (s_ourFiles.Contains(changed)) @@ -799,7 +795,8 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder _logger.LogInformation("Detected files changes."); - lock (SyncRoot) // don't reset while being locked + // don't reset while being locked + lock (SyncRoot) { ResetModels(); } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs index b1e5250992..0d5298e243 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; @@ -10,184 +8,182 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.ModelsBuilder; -using Umbraco.Cms.Infrastructure.WebAssets; -using Umbraco.Cms.Web.Common.ModelBinders; -namespace Umbraco.Cms.Web.Common.ModelsBuilder +namespace Umbraco.Cms.Web.Common.ModelsBuilder; + +/// +/// Handles and +/// notifications to initialize MB +/// +internal class ModelsBuilderNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler { - /// - /// Handles and notifications to initialize MB - /// - internal class ModelsBuilderNotificationHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly ModelsBuilderSettings _config; + private readonly IDefaultViewContentProvider _defaultViewContentProvider; + private readonly IModelsBuilderDashboardProvider _modelsBuilderDashboardProvider; + private readonly IShortStringHelper _shortStringHelper; + + public ModelsBuilderNotificationHandler( + IOptions config, + IShortStringHelper shortStringHelper, + IModelsBuilderDashboardProvider modelsBuilderDashboardProvider, + IDefaultViewContentProvider defaultViewContentProvider) { - private readonly ModelsBuilderSettings _config; - private readonly IShortStringHelper _shortStringHelper; - private readonly IModelsBuilderDashboardProvider _modelsBuilderDashboardProvider; - private readonly IDefaultViewContentProvider _defaultViewContentProvider; + _config = config.Value; + _shortStringHelper = shortStringHelper; + _modelsBuilderDashboardProvider = modelsBuilderDashboardProvider; + _defaultViewContentProvider = defaultViewContentProvider; + } - public ModelsBuilderNotificationHandler( - IOptions config, - IShortStringHelper shortStringHelper, - IModelsBuilderDashboardProvider modelsBuilderDashboardProvider, IDefaultViewContentProvider defaultViewContentProvider) + /// + /// Handles when a model binding error occurs + /// + public void Handle(ModelBindingErrorNotification notification) + { + ModelsBuilderAssemblyAttribute? sourceAttr = + notification.SourceType.Assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute? modelAttr = + notification.ModelType.Assembly.GetCustomAttribute(); + + // if source or model is not a ModelsBuider type... + if (sourceAttr == null || modelAttr == null) { - _config = config.Value; - _shortStringHelper = shortStringHelper; - _modelsBuilderDashboardProvider = modelsBuilderDashboardProvider; - _defaultViewContentProvider = defaultViewContentProvider; - } - - /// - /// Handles the notification to add custom urls and MB mode - /// - public void Handle(ServerVariablesParsingNotification notification) - { - IDictionary serverVars = notification.ServerVariables; - - if (!serverVars.ContainsKey("umbracoUrls")) - { - throw new ArgumentException("Missing umbracoUrls."); - } - - var umbracoUrlsObject = serverVars["umbracoUrls"]; - if (umbracoUrlsObject == null) - { - throw new ArgumentException("Null umbracoUrls"); - } - - if (!(umbracoUrlsObject is Dictionary umbracoUrls)) - { - throw new ArgumentException("Invalid umbracoUrls"); - } - - if (!serverVars.ContainsKey("umbracoPlugins")) - { - throw new ArgumentException("Missing umbracoPlugins."); - } - - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - { - throw new ArgumentException("Invalid umbracoPlugins"); - } - - umbracoUrls["modelsBuilderBaseUrl"] = _modelsBuilderDashboardProvider.GetUrl(); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); - } - - private Dictionary GetModelsBuilderSettings() - { - var settings = new Dictionary - { - {"mode", _config.ModelsMode.ToString() } - }; - - return settings; - } - - /// - /// Used to check if a template is being created based on a document type, in this case we need to - /// ensure the template markup is correct based on the model name of the document type - /// - public void Handle(TemplateSavingNotification notification) - { - if (_config.ModelsMode == ModelsMode.Nothing) + // if neither are ModelsBuilder types, give up entirely + if (sourceAttr == null && modelAttr == null) { return; } - // Don't do anything if we're not requested to create a template for a content type - if (notification.CreateTemplateForContentType is false) - { - return; - } - - // ensure we have the content type alias - if (notification.ContentTypeAlias is null) - { - throw new InvalidOperationException("ContentTypeAlias was not found on the notification"); - } - - foreach (ITemplate template in notification.SavedEntities) - { - // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key - // is found, then it means a new template is being created based on the creation of a document type - if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) - { - // ensure is safe and always pascal cased, per razor standard - // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application - var alias = notification.ContentTypeAlias; - var name = template.Name; // will be the name of the content type since we are creating - var className = UmbracoServices.GetClrName(_shortStringHelper, name, alias); - - var modelNamespace = _config.ModelsNamespace; - - // we do not support configuring this at the moment, so just let Umbraco use its default value - // var modelNamespaceAlias = ...; - var markup = _defaultViewContentProvider.GetDefaultFileContent( - modelClassName: className, - modelNamespace: modelNamespace/*, - modelNamespaceAlias: modelNamespaceAlias*/); - - // set the template content to the new markup - template.Content = markup; - } - } + // else report, but better not restart (loops?) + notification.Message.Append(" The "); + notification.Message.Append(sourceAttr == null ? "view model" : "source"); + notification.Message.Append(" is a ModelsBuilder type, but the "); + notification.Message.Append(sourceAttr != null ? "view model" : "source"); + notification.Message.Append(" is not. The application is in an unstable state and should be restarted."); + return; } - /// - /// Handles when a model binding error occurs - /// - public void Handle(ModelBindingErrorNotification notification) + // both are ModelsBuilder types + var pureSource = sourceAttr.IsInMemory; + var pureModel = modelAttr.IsInMemory; + + if (sourceAttr.IsInMemory || modelAttr.IsInMemory) { - ModelsBuilderAssemblyAttribute? sourceAttr = notification.SourceType.Assembly.GetCustomAttribute(); - ModelsBuilderAssemblyAttribute? modelAttr = notification.ModelType.Assembly.GetCustomAttribute(); - - // if source or model is not a ModelsBuider type... - if (sourceAttr == null || modelAttr == null) + if (pureSource == false || pureModel == false) { - // if neither are ModelsBuilder types, give up entirely - if (sourceAttr == null && modelAttr == null) - { - return; - } - - // else report, but better not restart (loops?) - notification.Message.Append(" The "); - notification.Message.Append(sourceAttr == null ? "view model" : "source"); - notification.Message.Append(" is a ModelsBuilder type, but the "); - notification.Message.Append(sourceAttr != null ? "view model" : "source"); - notification.Message.Append(" is not. The application is in an unstable state and should be restarted."); - return; + // only one is pure - report, but better not restart (loops?) + notification.Message.Append(pureSource + ? " The content model is in memory generated, but the view model is not." + : " The view model is in memory generated, but the content model is not."); + notification.Message.Append(" The application is in an unstable state and should be restarted."); } - - // both are ModelsBuilder types - var pureSource = sourceAttr.IsInMemory; - var pureModel = modelAttr.IsInMemory; - - if (sourceAttr.IsInMemory || modelAttr.IsInMemory) + else { - if (pureSource == false || pureModel == false) - { - // only one is pure - report, but better not restart (loops?) - notification.Message.Append(pureSource - ? " The content model is in memory generated, but the view model is not." - : " The view model is in memory generated, but the content model is not."); - notification.Message.Append(" The application is in an unstable state and should be restarted."); - } - else - { - // both are pure - report, and if different versions, restart - // if same version... makes no sense... and better not restart (loops?) - Version? sourceVersion = notification.SourceType.Assembly.GetName().Version; - Version? modelVersion = notification.ModelType.Assembly.GetName().Version; - notification.Message.Append(" Both view and content models are in memory generated, with "); - notification.Message.Append(sourceVersion == modelVersion - ? "same version. The application is in an unstable state and should be restarted." - : "different versions. The application is in an unstable state and should be restarted."); - } + // both are pure - report, and if different versions, restart + // if same version... makes no sense... and better not restart (loops?) + Version? sourceVersion = notification.SourceType.Assembly.GetName().Version; + Version? modelVersion = notification.ModelType.Assembly.GetName().Version; + notification.Message.Append(" Both view and content models are in memory generated, with "); + notification.Message.Append(sourceVersion == modelVersion + ? "same version. The application is in an unstable state and should be restarted." + : "different versions. The application is in an unstable state and should be restarted."); } } } + + /// + /// Handles the notification to add custom urls and MB mode + /// + public void Handle(ServerVariablesParsingNotification notification) + { + IDictionary serverVars = notification.ServerVariables; + + if (!serverVars.ContainsKey("umbracoUrls")) + { + throw new ArgumentException("Missing umbracoUrls."); + } + + var umbracoUrlsObject = serverVars["umbracoUrls"]; + if (umbracoUrlsObject == null) + { + throw new ArgumentException("Null umbracoUrls"); + } + + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + { + throw new ArgumentException("Invalid umbracoUrls"); + } + + if (!serverVars.ContainsKey("umbracoPlugins")) + { + throw new ArgumentException("Missing umbracoPlugins."); + } + + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + { + throw new ArgumentException("Invalid umbracoPlugins"); + } + + umbracoUrls["modelsBuilderBaseUrl"] = _modelsBuilderDashboardProvider.GetUrl(); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + } + + /// + /// Used to check if a template is being created based on a document type, in this case we need to + /// ensure the template markup is correct based on the model name of the document type + /// + public void Handle(TemplateSavingNotification notification) + { + if (_config.ModelsMode == ModelsMode.Nothing) + { + return; + } + + // Don't do anything if we're not requested to create a template for a content type + if (notification.CreateTemplateForContentType is false) + { + return; + } + + // ensure we have the content type alias + if (notification.ContentTypeAlias is null) + { + throw new InvalidOperationException("ContentTypeAlias was not found on the notification"); + } + + foreach (ITemplate template in notification.SavedEntities) + { + // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key + // is found, then it means a new template is being created based on the creation of a document type + if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) + { + // ensure is safe and always pascal cased, per razor standard + // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application + var alias = notification.ContentTypeAlias; + var name = template.Name; // will be the name of the content type since we are creating + var className = UmbracoServices.GetClrName(_shortStringHelper, name, alias); + + var modelNamespace = _config.ModelsNamespace; + + // we do not support configuring this at the moment, so just let Umbraco use its default value + // var modelNamespaceAlias = ...; + var markup = _defaultViewContentProvider.GetDefaultFileContent( + modelClassName: className, + modelNamespace: modelNamespace); /*, + modelNamespaceAlias: modelNamespaceAlias*/ + + // set the template content to the new markup + template.Content = markup; + } + } + } + + private Dictionary GetModelsBuilderSettings() + { + var settings = new Dictionary { { "mode", _config.ModelsMode.ToString() } }; + + return settings; + } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs index 8191c2aafb..1be67c575c 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Web.Common.ModelsBuilder +namespace Umbraco.Cms.Web.Common.ModelsBuilder; + +public class NoopModelsBuilderDashboardProvider : IModelsBuilderDashboardProvider { - public class NoopModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider - { - public string GetUrl() => string.Empty; - } + public string GetUrl() => string.Empty; } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs index aafdfa4bb5..6235c86fe3 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.ViewEngines; @@ -60,138 +58,137 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. */ -namespace Umbraco.Cms.Web.Common.ModelsBuilder +namespace Umbraco.Cms.Web.Common.ModelsBuilder; + +/// +/// Custom that wraps aspnetcore's default implementation +/// +/// +/// This is used so that when new models are built, the entire razor stack is re-constructed so all razor +/// caches and assembly references, etc... are cleared. +/// +internal class RefreshingRazorViewEngine : IRazorViewEngine, IDisposable { + private readonly Func _defaultRazorViewEngineFactory; + private readonly InMemoryModelFactory _inMemoryModelFactory; + private readonly ReaderWriterLockSlim _locker = new(); + private IRazorViewEngine _current; + private bool _disposedValue; + /// - /// Custom that wraps aspnetcore's default implementation + /// Initializes a new instance of the class. /// - /// - /// This is used so that when new models are built, the entire razor stack is re-constructed so all razor - /// caches and assembly references, etc... are cleared. - /// - internal class RefreshingRazorViewEngine : IRazorViewEngine, IDisposable + /// + /// A factory method used to re-construct the default aspnetcore + /// + /// The + public RefreshingRazorViewEngine( + Func defaultRazorViewEngineFactory, + InMemoryModelFactory inMemoryModelFactory) { - private IRazorViewEngine _current; - private bool _disposedValue; - private readonly InMemoryModelFactory _inMemoryModelFactory; - private readonly Func _defaultRazorViewEngineFactory; - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + _inMemoryModelFactory = inMemoryModelFactory; + _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory; + _current = _defaultRazorViewEngineFactory(); + _inMemoryModelFactory.ModelsChanged += InMemoryModelFactoryModelsChanged; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A factory method used to re-construct the default aspnetcore - /// - /// The - public RefreshingRazorViewEngine(Func defaultRazorViewEngineFactory, InMemoryModelFactory inMemoryModelFactory) + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + public RazorPageResult FindPage(ActionContext context, string pageName) + { + _locker.EnterReadLock(); + try + { + return _current.FindPage(context, pageName); + } + finally + { + _locker.ExitReadLock(); + } + } + + public string? GetAbsolutePath(string? executingFilePath, string? pagePath) + { + _locker.EnterReadLock(); + try + { + return _current.GetAbsolutePath(executingFilePath, pagePath); + } + finally + { + _locker.ExitReadLock(); + } + } + + public RazorPageResult GetPage(string executingFilePath, string pagePath) + { + _locker.EnterReadLock(); + try + { + return _current.GetPage(executingFilePath, pagePath); + } + finally + { + _locker.ExitReadLock(); + } + } + + public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) + { + _locker.EnterReadLock(); + try + { + return _current.FindView(context, viewName, isMainPage); + } + finally + { + _locker.ExitReadLock(); + } + } + + public ViewEngineResult GetView(string? executingFilePath, string viewPath, bool isMainPage) + { + _locker.EnterReadLock(); + try + { + return _current.GetView(executingFilePath, viewPath, isMainPage); + } + finally + { + _locker.ExitReadLock(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _inMemoryModelFactory.ModelsChanged -= InMemoryModelFactoryModelsChanged; + _locker.Dispose(); + } + + _disposedValue = true; + } + } + + /// + /// When the models change, re-construct the razor stack + /// + private void InMemoryModelFactoryModelsChanged(object? sender, EventArgs e) + { + _locker.EnterWriteLock(); + try { - _inMemoryModelFactory = inMemoryModelFactory; - _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory; _current = _defaultRazorViewEngineFactory(); - _inMemoryModelFactory.ModelsChanged += InMemoryModelFactoryModelsChanged; } - - /// - /// When the models change, re-construct the razor stack - /// - private void InMemoryModelFactoryModelsChanged(object? sender, EventArgs e) + finally { - _locker.EnterWriteLock(); - try - { - _current = _defaultRazorViewEngineFactory(); - } - finally - { - _locker.ExitWriteLock(); - } - } - - public RazorPageResult FindPage(ActionContext context, string pageName) - { - _locker.EnterReadLock(); - try - { - return _current.FindPage(context, pageName); - } - finally - { - _locker.ExitReadLock(); - } - } - - public string? GetAbsolutePath(string? executingFilePath, string? pagePath) - { - _locker.EnterReadLock(); - try - { - return _current.GetAbsolutePath(executingFilePath, pagePath); - } - finally - { - _locker.ExitReadLock(); - } - } - - public RazorPageResult GetPage(string executingFilePath, string pagePath) - { - _locker.EnterReadLock(); - try - { - return _current.GetPage(executingFilePath, pagePath); - } - finally - { - _locker.ExitReadLock(); - } - } - - public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) - { - _locker.EnterReadLock(); - try - { - return _current.FindView(context, viewName, isMainPage); - - } - finally - { - _locker.ExitReadLock(); - } - } - - public ViewEngineResult GetView(string? executingFilePath, string viewPath, bool isMainPage) - { - _locker.EnterReadLock(); - try - { - return _current.GetView(executingFilePath, viewPath, isMainPage); - } - finally - { - _locker.ExitReadLock(); - } - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _inMemoryModelFactory.ModelsChanged -= InMemoryModelFactoryModelsChanged; - _locker.Dispose(); - } - - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _locker.ExitWriteLock(); } } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs index 75c08e8772..17f148002e 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs @@ -1,23 +1,22 @@ using System.Reflection; using System.Runtime.Loader; -namespace Umbraco.Cms.Web.Common.ModelsBuilder -{ - internal class UmbracoAssemblyLoadContext : AssemblyLoadContext - { - /// - /// Initializes a new instance of the class. - /// - /// - /// Collectible AssemblyLoadContext used to load in the compiled generated models. - /// Must be a collectible assembly in order to be able to be unloaded. - /// - public UmbracoAssemblyLoadContext() - : base(isCollectible: true) - { - } +namespace Umbraco.Cms.Web.Common.ModelsBuilder; - // we never load anything directly by assembly name. This method will never be called - protected override Assembly? Load(AssemblyName assemblyName) => null; +internal class UmbracoAssemblyLoadContext : AssemblyLoadContext +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// Collectible AssemblyLoadContext used to load in the compiled generated models. + /// Must be a collectible assembly in order to be able to be unloaded. + /// + public UmbracoAssemblyLoadContext() + : base(true) + { } + + // we never load anything directly by assembly name. This method will never be called + protected override Assembly? Load(AssemblyName assemblyName) => null; } diff --git a/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs index 2194572e77..c043161486 100644 --- a/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs +++ b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs @@ -1,328 +1,342 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; using System.Text; using HtmlAgilityPack; using Microsoft.AspNetCore.Html; -namespace Umbraco.Cms.Web.Common.Mvc +namespace Umbraco.Cms.Web.Common.Mvc; + +/// +/// Provides utility methods for UmbracoHelper for working with strings and HTML in views. +/// +public sealed class HtmlStringUtilities { /// - /// Provides utility methods for UmbracoHelper for working with strings and HTML in views. + /// HTML encodes the text and replaces text line breaks with HTML line breaks. /// - public sealed class HtmlStringUtilities + /// The text. + /// + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + /// + public IHtmlContent ReplaceLineBreaks(string text) { - /// - /// HTML encodes the text and replaces text line breaks with HTML line breaks. - /// - /// The text. - /// - /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). - /// - public IHtmlContent ReplaceLineBreaks(string text) + var value = WebUtility.HtmlEncode(text) + .Replace("\r\n", "
") + .Replace("\r", "
") + .Replace("\n", "
"); + + return new HtmlString(value); + } + + public HtmlString StripHtmlTags(string html, params string[]? tags) + { + var doc = new HtmlDocument(); + doc.LoadHtml("

" + html + "

"); + + var targets = new List(); + + HtmlNodeCollection? nodes = doc.DocumentNode.FirstChild.SelectNodes(".//*"); + if (nodes != null) { - var value = WebUtility.HtmlEncode(text)? - .Replace("\r\n", "
") - .Replace("\r", "
") - .Replace("\n", "
"); - - return new HtmlString(value); - } - - public HtmlString StripHtmlTags(string html, params string[]? tags) - { - var doc = new HtmlDocument(); - doc.LoadHtml("

" + html + "

"); - - var targets = new List(); - - var nodes = doc.DocumentNode.FirstChild.SelectNodes(".//*"); - if (nodes != null) + foreach (HtmlNode? node in nodes) { - foreach (var node in nodes) + // is element + if (node.NodeType != HtmlNodeType.Element) { - //is element - if (node.NodeType != HtmlNodeType.Element) continue; - var filterAllTags = (tags == null || !tags.Any()); - if (filterAllTags || (tags?.Any(tag => string.Equals(tag, node.Name, StringComparison.CurrentCultureIgnoreCase)) ?? false)) - { - targets.Add(node); - } + continue; } - foreach (var target in targets) + + var filterAllTags = tags == null || !tags.Any(); + if (filterAllTags || + (tags?.Any(tag => string.Equals(tag, node.Name, StringComparison.CurrentCultureIgnoreCase)) ?? + false)) { - HtmlNode content = doc.CreateTextNode(target.InnerText); - target.ParentNode.ReplaceChild(content, target); + targets.Add(node); } } - else + + foreach (HtmlNode target in targets) { - return new HtmlString(html); + HtmlNode content = doc.CreateTextNode(target.InnerText); + target.ParentNode.ReplaceChild(content, target); } - return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml.Replace(" ", " ")); + } + else + { + return new HtmlString(html); } - public string Join(string separator, params object[] args) + return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml.Replace(" ", " ")); + } + + public string Join(string separator, params object[] args) + { + IEnumerable results = args + .Select(x => x.ToString()) + .Where(x => string.IsNullOrWhiteSpace(x) == false); + return string.Join(separator, results); + } + + public string Concatenate(params object[] args) + { + var sb = new StringBuilder(); + foreach (var arg in args + .Select(x => x.ToString()) + .Where(x => string.IsNullOrWhiteSpace(x) == false)) { - var results = args - .Where(x => x != null) - .Select(x => x.ToString()) - .Where(x => string.IsNullOrWhiteSpace(x) == false); - return string.Join(separator, results); + sb.Append(arg); } - public string Concatenate(params object[] args) + return sb.ToString(); + } + + public string Coalesce(params object[] args) + { + var arg = args + .Select(x => x.ToString()) + .FirstOrDefault(x => string.IsNullOrWhiteSpace(x) == false); + + return arg ?? string.Empty; + } + + public IHtmlContent Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent) + { + const string hellip = "…"; + + using (var outputms = new MemoryStream()) { - var sb = new StringBuilder(); - foreach (var arg in args - .Where(x => x != null) - .Select(x => x.ToString()) - .Where(x => string.IsNullOrWhiteSpace(x) == false)) + var lengthReached = false; + + using (var outputtw = new StreamWriter(outputms)) { - sb.Append(arg); - } - return sb.ToString(); - } - - public string Coalesce(params object[] args) - { - var arg = args - .Where(x => x != null) - .Select(x => x.ToString()) - .FirstOrDefault(x => string.IsNullOrWhiteSpace(x) == false); - - return arg ?? string.Empty; - } - - public IHtmlContent Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent) - { - const string hellip = "…"; - - using (var outputms = new MemoryStream()) - { - bool lengthReached = false; - - using (var outputtw = new StreamWriter(outputms)) + using (var ms = new MemoryStream()) { - using (var ms = new MemoryStream()) + using (var tw = new StreamWriter(ms)) { - using (var tw = new StreamWriter(ms)) + tw.Write(html); + tw.Flush(); + ms.Position = 0; + var tagStack = new Stack(); + + using (TextReader tr = new StreamReader(ms)) { - tw.Write(html); - tw.Flush(); - ms.Position = 0; - var tagStack = new Stack(); + bool isInsideElement = false, + insideTagSpaceEncountered = false, + isTagClose = false; - using (TextReader tr = new StreamReader(ms)) + int ic, + + // currentLength = 0, + currentTextLength = 0; + + string currentTag = string.Empty, + tagContents = string.Empty; + + while ((ic = tr.Read()) != -1) { - bool isInsideElement = false, - insideTagSpaceEncountered = false, - isTagClose = false; + var write = true; - int ic = 0, - //currentLength = 0, - currentTextLength = 0; - - string currentTag = string.Empty, - tagContents = string.Empty; - - while ((ic = tr.Read()) != -1) + switch ((char)ic) { - bool write = true; + case '<': + if (!lengthReached) + { + isInsideElement = true; + } - switch ((char)ic) - { - case '<': + insideTagSpaceEncountered = false; + currentTag = string.Empty; + tagContents = string.Empty; + isTagClose = false; + if (tr.Peek() == '/') + { + isTagClose = true; + } + + break; + + case '>': + isInsideElement = false; + + if (isTagClose && tagStack.Count > 0) + { + var thisTag = tagStack.Pop(); + outputtw.Write(""); + if (treatTagsAsContent) + { + currentTextLength++; + } + } + + if (!isTagClose && currentTag.Length > 0) + { if (!lengthReached) { - isInsideElement = true; - } - - insideTagSpaceEncountered = false; - currentTag = string.Empty; - tagContents = string.Empty; - isTagClose = false; - if (tr.Peek() == (int)'/') - { - isTagClose = true; - } - break; - - case '>': - isInsideElement = false; - - if (isTagClose && tagStack.Count > 0) - { - string thisTag = tagStack.Pop(); - outputtw.Write(""); + tagStack.Push(currentTag); + outputtw.Write("<" + currentTag); if (treatTagsAsContent) { currentTextLength++; } - } - if (!isTagClose && currentTag.Length > 0) - { - if (!lengthReached) - { - tagStack.Push(currentTag); - outputtw.Write("<" + currentTag); - if (treatTagsAsContent) - { - currentTextLength++; - } - if (!string.IsNullOrEmpty(tagContents)) - { - if (tagContents.EndsWith("/")) - { - // No end tag e.g.
. - tagStack.Pop(); - } - outputtw.Write(tagContents); - write = true; - insideTagSpaceEncountered = false; - } - outputtw.Write(">"); - } - } - // Continue to next iteration of the text reader. - continue; - - default: - if (isInsideElement) - { - if (ic == (int)' ') + if (!string.IsNullOrEmpty(tagContents)) { - if (!insideTagSpaceEncountered) + if (tagContents.EndsWith("/")) { - insideTagSpaceEncountered = true; + // No end tag e.g.
. + tagStack.Pop(); } + + outputtw.Write(tagContents); + insideTagSpaceEncountered = false; } + outputtw.Write(">"); + } + } + + // Continue to next iteration of the text reader. + continue; + + default: + if (isInsideElement) + { + if (ic == ' ') + { if (!insideTagSpaceEncountered) { - currentTag += (char)ic; + insideTagSpaceEncountered = true; } } - break; - } - if (isInsideElement || insideTagSpaceEncountered) - { - write = false; - if (insideTagSpaceEncountered) - { - tagContents += (char)ic; - } - } - - if (!isInsideElement || treatTagsAsContent) - { - currentTextLength++; - } - - if (currentTextLength <= length || (lengthReached && isInsideElement)) - { - if (write) - { - var charToWrite = (char)ic; - outputtw.Write(charToWrite); - //currentLength++; - } - } - - if (!lengthReached) - { - if (currentTextLength == length) - { - // if the last character added was the first of a two character unicode pair, add the second character - if (char.IsHighSurrogate((char)ic)) + if (!insideTagSpaceEncountered) { - var lowSurrogate = tr.Read(); - outputtw.Write((char)lowSurrogate); + currentTag += (char)ic; } - } - // only add elipsis if current length greater than original length - if (currentTextLength > length) - { - if (addElipsis) - { - outputtw.Write(hellip); - } - lengthReached = true; - } - } + break; } + if (isInsideElement || insideTagSpaceEncountered) + { + write = false; + if (insideTagSpaceEncountered) + { + tagContents += (char)ic; + } + } + + if (!isInsideElement || treatTagsAsContent) + { + currentTextLength++; + } + + if (currentTextLength <= length || (lengthReached && isInsideElement)) + { + if (write) + { + var charToWrite = (char)ic; + outputtw.Write(charToWrite); + + // currentLength++; + } + } + + if (!lengthReached) + { + if (currentTextLength == length) + { + // if the last character added was the first of a two character unicode pair, add the second character + if (char.IsHighSurrogate((char)ic)) + { + var lowSurrogate = tr.Read(); + outputtw.Write((char)lowSurrogate); + } + } + + // only add elipsis if current length greater than original length + if (currentTextLength > length) + { + if (addElipsis) + { + outputtw.Write(hellip); + } + + lengthReached = true; + } + } } } } - outputtw.Flush(); - outputms.Position = 0; - using (TextReader outputtr = new StreamReader(outputms)) + } + + outputtw.Flush(); + outputms.Position = 0; + using (TextReader outputtr = new StreamReader(outputms)) + { + string result; + + var firstTrim = outputtr.ReadToEnd().Replace(" ", " ").Trim(); + + // Check to see if there is an empty char between the hellip and the output string + // if there is, remove it + if (addElipsis && lengthReached && string.IsNullOrWhiteSpace(firstTrim) == false) { - string result = string.Empty; - - string firstTrim = outputtr.ReadToEnd().Replace(" ", " ").Trim(); - - // Check to see if there is an empty char between the hellip and the output string - // if there is, remove it - if (addElipsis && lengthReached && string.IsNullOrWhiteSpace(firstTrim) == false) - { - result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) : firstTrim; - } - else - { - result = firstTrim; - } - - return new HtmlString(result); + result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' + ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) + : firstTrim; } + else + { + result = firstTrim; + } + + return new HtmlString(result); } } } - - /// - /// Returns the length of the words from a HTML block - /// - /// HTML text - /// Amount of words you would like to measure - /// - /// - public int WordsToLength(string html, int words) - { - HtmlDocument doc = new HtmlDocument(); - doc.LoadHtml(html); - - int wordCount = 0, - length = 0, - maxWords = words; - - html = StripHtmlTags(html, null).ToString(); - - while (length < html.Length) - { - // Check to see if the current wordCount reached the maxWords allowed - if (wordCount.Equals(maxWords)) break; - // Check if current char is part of a word - while (length < html.Length && char.IsWhiteSpace(html[length]) == false) - { - length++; - } - - wordCount++; - - // Skip whitespace until the next word - while (length < html.Length && char.IsWhiteSpace(html[length]) && wordCount.Equals(maxWords) == false) - { - length++; - } - } - return length; - } + } + + /// + /// Returns the length of the words from a HTML block + /// + /// HTML text + /// Amount of words you would like to measure + /// + public int WordsToLength(string html, int words) + { + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + int wordCount = 0, + length = 0, + maxWords = words; + + html = StripHtmlTags(html, null).ToString(); + + while (length < html.Length) + { + // Check to see if the current wordCount reached the maxWords allowed + if (wordCount.Equals(maxWords)) + { + break; + } + + // Check if current char is part of a word + while (length < html.Length && char.IsWhiteSpace(html[length]) == false) + { + length++; + } + + wordCount++; + + // Skip whitespace until the next word + while (length < html.Length && char.IsWhiteSpace(html[length]) && wordCount.Equals(maxWords) == false) + { + length++; + } + } + + return length; } } diff --git a/src/Umbraco.Web.Common/Mvc/IpAddressUtilities.cs b/src/Umbraco.Web.Common/Mvc/IpAddressUtilities.cs index ab9ef5d0b9..876c1fdd3f 100644 --- a/src/Umbraco.Web.Common/Mvc/IpAddressUtilities.cs +++ b/src/Umbraco.Web.Common/Mvc/IpAddressUtilities.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.Mvc; diff --git a/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs index 15927a4404..246e313b9a 100644 --- a/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs +++ b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs @@ -3,23 +3,21 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.ModelBinders; -namespace Umbraco.Cms.Web.Common.Mvc -{ +namespace Umbraco.Cms.Web.Common.Mvc; - /// - /// Options for globally configuring MVC for Umbraco - /// - /// - /// We generally don't want to change the global MVC settings since we want to be unobtrusive as possible but some - /// global mods are needed - so long as they don't interfere with normal user usages of MVC. - /// - public class UmbracoMvcConfigureOptions : IConfigureOptions +/// +/// Options for globally configuring MVC for Umbraco +/// +/// +/// We generally don't want to change the global MVC settings since we want to be unobtrusive as possible but some +/// global mods are needed - so long as they don't interfere with normal user usages of MVC. +/// +public class UmbracoMvcConfigureOptions : IConfigureOptions +{ + /// + public void Configure(MvcOptions options) { - /// - public void Configure(MvcOptions options) - { - options.ModelBinderProviders.Insert(0, new ContentModelBinderProvider()); - options.Filters.Insert(0, new EnsurePartialViewMacroViewContextFilterAttribute()); - } + options.ModelBinderProviders.Insert(0, new ContentModelBinderProvider()); + options.Filters.Insert(0, new EnsurePartialViewMacroViewContextFilterAttribute()); } } diff --git a/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs b/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs index 1896d39745..ef01593704 100644 --- a/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs +++ b/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs @@ -1,59 +1,59 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; -using System.Linq; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Plugins +namespace Umbraco.Cms.Web.Common.Plugins; + +/// +/// Looks up files using the on-disk file system and check file extensions are on a allow list +/// +/// +/// When the environment variable "DOTNET_USE_POLLING_FILE_WATCHER" is set to "1" or "true", calls to +/// will use . +/// +public class UmbracoPluginPhysicalFileProvider : PhysicalFileProvider, IFileProvider { + private UmbracoPluginSettings _options; + /// - /// Looks up files using the on-disk file system and check file extensions are on a allow list + /// Initializes a new instance of the class, at the given root + /// directory. + /// + /// The root directory. This should be an absolute path. + /// The configuration options. + /// Specifies which files or directories are excluded. + public UmbracoPluginPhysicalFileProvider(string root, IOptionsMonitor options, ExclusionFilters filters = ExclusionFilters.Sensitive) + : base(root, filters) + { + _options = options.CurrentValue; + options.OnChange(x => + { + _options = x; + }); + } + + /// + /// Locate a file at the given path by directly mapping path segments to physical directories. /// /// - /// When the environment variable "DOTNET_USE_POLLING_FILE_WATCHER" is set to "1" or "true", calls to - /// will use . + /// The path needs to pass the and the + /// to be found. /// - public class UmbracoPluginPhysicalFileProvider : PhysicalFileProvider, IFileProvider + /// A path under the root directory + /// The file information. Caller must check property. + public new IFileInfo GetFileInfo(string subpath) { - private UmbracoPluginSettings _options; - - /// - /// Initializes a new instance of the class, at the given root directory. - /// - /// The root directory. This should be an absolute path. - /// The configuration options. - /// Specifies which files or directories are excluded. - public UmbracoPluginPhysicalFileProvider(string root, IOptionsMonitor options, ExclusionFilters filters = ExclusionFilters.Sensitive) - : base(root, filters) + var extension = Path.GetExtension(subpath); + var subPathInclAppPluginsFolder = Path.Combine(Core.Constants.SystemDirectories.AppPlugins, subpath); + if (!_options.BrowsableFileExtensions.Contains(extension)) { - _options = options.CurrentValue; - options.OnChange(x => { - _options = x; - } ); + return new NotFoundFileInfo(subPathInclAppPluginsFolder); } - /// - /// Locate a file at the given path by directly mapping path segments to physical directories. - /// - /// - /// The path needs to pass the and the to be found. - /// - /// A path under the root directory - /// The file information. Caller must check property. - public new IFileInfo GetFileInfo(string subpath) - { - var extension = Path.GetExtension(subpath); - var subPathInclAppPluginsFolder = Path.Combine(Cms.Core.Constants.SystemDirectories.AppPlugins, subpath); - if (!_options.BrowsableFileExtensions.Contains(extension)) - { - return new NotFoundFileInfo(subPathInclAppPluginsFolder); - } - - return base.GetFileInfo(subPathInclAppPluginsFolder); - } + return base.GetFileInfo(subPathInclAppPluginsFolder); } } diff --git a/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs b/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs index cdd6d0aee9..6258471140 100644 --- a/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs +++ b/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Logging; @@ -7,50 +7,48 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Web.Common.Profiler +namespace Umbraco.Cms.Web.Common.Profiler; + +/// +/// Initialized the web profiling. Ensures the boot process profiling is stopped. +/// +public class InitializeWebProfiling : INotificationHandler { + private readonly bool _profile; + private readonly WebProfiler? _profiler; + /// - /// Initialized the web profiling. Ensures the boot process profiling is stopped. + /// Initializes a new instance of the class. /// - public class InitializeWebProfiling : INotificationHandler + public InitializeWebProfiling(IProfiler profiler, ILogger logger) { - private readonly bool _profile; - private readonly WebProfiler? _profiler; + _profile = true; - /// - /// Initializes a new instance of the class. - /// - public InitializeWebProfiling(IProfiler profiler, ILogger logger) + // although registered in UmbracoBuilderExtensions.AddUmbraco, ensure that we have not + // been replaced by another component, and we are still "the" profiler + _profiler = profiler as WebProfiler; + if (_profiler != null) { - _profile = true; - - // although registered in UmbracoBuilderExtensions.AddUmbraco, ensure that we have not - // been replaced by another component, and we are still "the" profiler - _profiler = profiler as WebProfiler; - if (_profiler != null) - { - return; - } - - // if VoidProfiler was registered, let it be known - if (profiler is NoopProfiler) - { - logger.LogInformation( - "Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); - } - - _profile = false; + return; } - /// - public void Handle(UmbracoApplicationStartingNotification notification) + // if VoidProfiler was registered, let it be known + if (profiler is NoopProfiler) { - if (_profile && notification.RuntimeLevel == RuntimeLevel.Run) - { - // Stop the profiling of the booting process - _profiler?.StopBoot(); - } + logger.LogInformation( + "Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); } + _profile = false; + } + + /// + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_profile && notification.RuntimeLevel == RuntimeLevel.Run) + { + // Stop the profiling of the booting process + _profiler?.StopBoot(); + } } } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index fe1aa710d4..9608bad715 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,133 +1,140 @@ -using System; -using System.Linq; using System.Net; -using System.Threading; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using StackExchange.Profiling; using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Profiler +namespace Umbraco.Cms.Web.Common.Profiler; + +public class WebProfiler : IProfiler { - - public class WebProfiler : IProfiler + public static readonly AsyncLocal MiniProfilerContext = new(x => { - private const string WebProfileCookieKey = "umbracoWebProfiler"; + _ = x; + }); - public static readonly AsyncLocal MiniProfilerContext = new AsyncLocal(x => + private const string WebProfileCookieKey = "umbracoWebProfiler"; + + private int _first; + private MiniProfiler? _startupProfiler; + + public IDisposable? Step(string name) => MiniProfiler.Current?.Step(name); + + public void Start() + { + MiniProfiler.StartNew(); + MiniProfilerContext.Value = MiniProfiler.Current; + } + + public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); + + public void StartBoot() => _startupProfiler = MiniProfiler.StartNew("Startup Profiler"); + + public void StopBoot() => _startupProfiler?.Stop(); + + public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) + { + if (runtimeLevel != RuntimeLevel.Run) { - _ = x; - }); - private MiniProfiler? _startupProfiler; - private int _first; - - - - public IDisposable? Step(string name) - { - return MiniProfiler.Current?.Step(name); + return; } - public void Start() + if (ShouldProfile(context.Request)) { - MiniProfiler.StartNew(); - MiniProfilerContext.Value = MiniProfiler.Current; + Start(); + ICookieManager cookieManager = GetCookieManager(context); + cookieManager.ExpireCookie( + WebProfileCookieKey); // Ensure we expire the cookie, so we do not reuse the old potential value saved + } + } + + public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) + { + if (runtimeLevel != RuntimeLevel.Run) + { + return; } - public void StartBoot() => _startupProfiler = MiniProfiler.StartNew("Startup Profiler"); - - public void StopBoot() => _startupProfiler?.Stop(); - - public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); - - public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) + if (ShouldProfile(context.Request)) { - if (runtimeLevel != RuntimeLevel.Run) - { - return; - } + Stop(); - if (ShouldProfile(context.Request)) - { - Start(); - ICookieManager cookieManager = GetCookieManager(context); - cookieManager.ExpireCookie(WebProfileCookieKey); //Ensure we expire the cookie, so we do not reuse the old potential value saved - } - } - - private static ICookieManager GetCookieManager(HttpContext context) => context.RequestServices.GetRequiredService(); - - public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) - { - if (runtimeLevel != RuntimeLevel.Run) - { - return; - } - - if (ShouldProfile(context.Request)) - { - Stop(); - - if (MiniProfilerContext.Value is not null) - { - // if this is the first request, append the startup profiler - var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first) - { - if (_startupProfiler is not null) - { - AddSubProfiler(_startupProfiler); - } - - _startupProfiler = null; - } - - ICookieManager cookieManager = GetCookieManager(context); - var cookieValue = cookieManager.GetCookieValue(WebProfileCookieKey); - - if (cookieValue is not null) - { - AddSubProfiler(MiniProfiler.FromJson(cookieValue)); - } - - //If it is a redirect to a relative path (local redirect) - if (context.Response.StatusCode == (int)HttpStatusCode.Redirect - && context.Response.Headers.TryGetValue(Microsoft.Net.Http.Headers.HeaderNames.Location, out var location) - && !location.Contains("://")) - { - MiniProfilerContext.Value.Root.Name = "Before Redirect"; - cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); - } - - } - - } - } - - private void AddSubProfiler(MiniProfiler subProfiler) - { - var startupDuration = subProfiler.Root.DurationMilliseconds.GetValueOrDefault(); if (MiniProfilerContext.Value is not null) { - MiniProfilerContext.Value.DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.Root.AddChild(subProfiler.Root); + // if this is the first request, append the startup profiler + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + if (_startupProfiler is not null) + { + AddSubProfiler(_startupProfiler); + } + + _startupProfiler = null; + } + + ICookieManager cookieManager = GetCookieManager(context); + var cookieValue = cookieManager.GetCookieValue(WebProfileCookieKey); + + if (cookieValue is not null) + { + AddSubProfiler(MiniProfiler.FromJson(cookieValue)); + } + + // If it is a redirect to a relative path (local redirect) + if (context.Response.StatusCode == (int)HttpStatusCode.Redirect + && context.Response.Headers.TryGetValue(HeaderNames.Location, out StringValues location) + && !location.Contains("://")) + { + MiniProfilerContext.Value.Root.Name = "Before Redirect"; + cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); + } } } + } - private static bool ShouldProfile(HttpRequest request) + private static ICookieManager GetCookieManager(HttpContext context) => + context.RequestServices.GetRequiredService(); + + private static bool ShouldProfile(HttpRequest request) + { + if (request.IsClientSideRequest()) { - if (request.IsClientSideRequest()) return false; - if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; - if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; - if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; return false; } + + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) + { + return umbDebug; + } + + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) + { + return xUmbDebug; + } + + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) + { + return cUmbDebug; + } + + return false; + } + + private void AddSubProfiler(MiniProfiler subProfiler) + { + var startupDuration = subProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + if (MiniProfilerContext.Value is not null) + { + MiniProfilerContext.Value.DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.Root.AddChild(subProfiler.Root); + } } } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs index f061c58242..733715cf53 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs @@ -1,50 +1,49 @@ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using StackExchange.Profiling; using StackExchange.Profiling.Internal; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Web.Common.Profiler +namespace Umbraco.Cms.Web.Common.Profiler; + +public class WebProfilerHtml : IProfilerHtml { - public class WebProfilerHtml : IProfilerHtml + private readonly IHttpContextAccessor _httpContextAccessor; + + public WebProfilerHtml(IHttpContextAccessor httpContextAccessor) => + + // create our own provider, which can provide a profiler even during boot + _httpContextAccessor = httpContextAccessor; + + /// + /// + /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this + /// method does not work. + /// We fake the requestIds from the RequestState here. + /// + public string Render() { - private readonly IHttpContextAccessor _httpContextAccessor; - - public WebProfilerHtml(IHttpContextAccessor httpContextAccessor) + MiniProfiler? profiler = MiniProfiler.Current; + if (profiler == null) { - // create our own provider, which can provide a profiler even during boot - _httpContextAccessor = httpContextAccessor; + return string.Empty; } - /// - /// - /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work. - /// We fake the requestIds from the RequestState here. - /// - public string Render() - { + HttpContext? context = _httpContextAccessor.HttpContext; - var profiler = MiniProfiler.Current; - if (profiler == null) return string.Empty; + var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); - var context = _httpContextAccessor.HttpContext; + var result = StackExchange.Profiling.Internal.Render.Includes( + profiler, + context is not null ? context.Request.PathBase + path : null, + true, + new List { profiler.Id }, + RenderPosition.Right, + profiler.Options.PopupShowTrivial, + profiler.Options.PopupShowTimeWithChildren, + profiler.Options.PopupMaxTracesToShow, + profiler.Options.ShowControls, + profiler.Options.PopupStartHidden); - var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); - - var result = StackExchange.Profiling.Internal.Render.Includes( - profiler, - path: context is not null ? context.Request.PathBase + path : null, - isAuthorized: true, - requestIDs: new List { profiler.Id }, - position: RenderPosition.Right, - showTrivial: profiler.Options.PopupShowTrivial, - showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren, - maxTracesToShow: profiler.Options.PopupMaxTracesToShow, - showControls: profiler.Options.ShowControls, - startHidden: profiler.Options.PopupStartHidden); - - return result; - } + return result; } } diff --git a/src/Umbraco.Web.Common/PublishedModels/DummyClassSoThatPublishedModelsNamespaceExists.cs b/src/Umbraco.Web.Common/PublishedModels/DummyClassSoThatPublishedModelsNamespaceExists.cs index df1be4a6f1..2d83b9013a 100644 --- a/src/Umbraco.Web.Common/PublishedModels/DummyClassSoThatPublishedModelsNamespaceExists.cs +++ b/src/Umbraco.Web.Common/PublishedModels/DummyClassSoThatPublishedModelsNamespaceExists.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Web.Common.PublishedModels +namespace Umbraco.Cms.Web.Common.PublishedModels; + +// this is here so that Umbraco.Web.PublishedModels namespace exists in views +// even if people are not using models at all - because we are referencing it +// when compiling views - hopefully noone will ever create an actual model +// with that name +internal class DummyClassSoThatPublishedModelsNamespaceExists { - // this is here so that Umbraco.Web.PublishedModels namespace exists in views - // even if people are not using models at all - because we are referencing it - // when compiling views - hopefully noone will ever create an actual model - // with that name - internal class DummyClassSoThatPublishedModelsNamespaceExists - { } } diff --git a/src/Umbraco.Web.Common/Routing/CustomRouteContentFinderDelegate.cs b/src/Umbraco.Web.Common/Routing/CustomRouteContentFinderDelegate.cs index 7f835f7996..e42e1e63a7 100644 --- a/src/Umbraco.Web.Common/Routing/CustomRouteContentFinderDelegate.cs +++ b/src/Umbraco.Web.Common/Routing/CustomRouteContentFinderDelegate.cs @@ -1,15 +1,15 @@ -using System; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Web.Common.Routing +namespace Umbraco.Cms.Web.Common.Routing; + +internal class CustomRouteContentFinderDelegate { - internal class CustomRouteContentFinderDelegate - { - private readonly Func _findContent; + private readonly Func _findContent; - public CustomRouteContentFinderDelegate(Func findContent) => _findContent = findContent; + public CustomRouteContentFinderDelegate(Func findContent) => + _findContent = findContent; - public IPublishedContent FindContent(ActionExecutingContext actionExecutingContext) => _findContent(actionExecutingContext); - } + public IPublishedContent FindContent(ActionExecutingContext actionExecutingContext) => + _findContent(actionExecutingContext); } diff --git a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs index a82e81f34f..00e763dffb 100644 --- a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs +++ b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs @@ -1,20 +1,19 @@ using Microsoft.AspNetCore.Routing; -namespace Umbraco.Cms.Web.Common.Routing -{ - /// - /// Used to create routes for a route area - /// - public interface IAreaRoutes - { - // TODO: It could be possible to just get all collections of IAreaRoutes and route them all instead of relying - // on individual ext methods. This would reduce the amount of code in Startup, but could also mean there's less control over startup - // if someone wanted that. Maybe we can just have both. +namespace Umbraco.Cms.Web.Common.Routing; - /// - /// Create routes for an area - /// - /// The endpoint route builder - void CreateRoutes(IEndpointRouteBuilder endpoints); - } +/// +/// Used to create routes for a route area +/// +public interface IAreaRoutes +{ + // TODO: It could be possible to just get all collections of IAreaRoutes and route them all instead of relying + // on individual ext methods. This would reduce the amount of code in Startup, but could also mean there's less control over startup + // if someone wanted that. Maybe we can just have both. + + /// + /// Create routes for an area + /// + /// The endpoint route builder + void CreateRoutes(IEndpointRouteBuilder endpoints); } diff --git a/src/Umbraco.Web.Common/Routing/IRoutableDocumentFilter.cs b/src/Umbraco.Web.Common/Routing/IRoutableDocumentFilter.cs index 62b52191e8..63995da524 100644 --- a/src/Umbraco.Web.Common/Routing/IRoutableDocumentFilter.cs +++ b/src/Umbraco.Web.Common/Routing/IRoutableDocumentFilter.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Web.Common.Routing +namespace Umbraco.Cms.Web.Common.Routing; + +public interface IRoutableDocumentFilter { - public interface IRoutableDocumentFilter - { - bool IsDocumentRequest(string absPath); - } + bool IsDocumentRequest(string absPath); } diff --git a/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs b/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs index 355c73c663..44fa64b274 100644 --- a/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs +++ b/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs @@ -1,9 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Options; @@ -11,200 +6,202 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Routing -{ - /// - /// Utility class used to check if the current request is for a front-end request - /// - /// - /// There are various checks to determine if this is a front-end request such as checking if the request is part of any reserved paths or existing MVC routes. - /// - public sealed class RoutableDocumentFilter : IRoutableDocumentFilter - { - private readonly ConcurrentDictionary _routeChecks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly GlobalSettings _globalSettings; - private readonly WebRoutingSettings _routingSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly EndpointDataSource _endpointDataSource; - private readonly object _routeLocker = new object(); - private object _initLocker = new object(); - private bool _isInit = false; - private HashSet? _reservedList; +namespace Umbraco.Cms.Web.Common.Routing; - /// - /// Initializes a new instance of the class. - /// - public RoutableDocumentFilter(IOptions globalSettings, IOptions routingSettings, IHostingEnvironment hostingEnvironment, EndpointDataSource endpointDataSource) +/// +/// Utility class used to check if the current request is for a front-end request +/// +/// +/// There are various checks to determine if this is a front-end request such as checking if the request is part of any +/// reserved paths or existing MVC routes. +/// +public sealed class RoutableDocumentFilter : IRoutableDocumentFilter +{ + private readonly EndpointDataSource _endpointDataSource; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ConcurrentDictionary _routeChecks = new(StringComparer.OrdinalIgnoreCase); + private readonly object _routeLocker = new(); + private readonly WebRoutingSettings _routingSettings; + private object _initLocker = new(); + private bool _isInit; + private HashSet? _reservedList; + + /// + /// Initializes a new instance of the class. + /// + public RoutableDocumentFilter(IOptions globalSettings, IOptions routingSettings, IHostingEnvironment hostingEnvironment, EndpointDataSource endpointDataSource) + { + _globalSettings = globalSettings.Value; + _routingSettings = routingSettings.Value; + _hostingEnvironment = hostingEnvironment; + _endpointDataSource = endpointDataSource; + _endpointDataSource.GetChangeToken().RegisterChangeCallback(EndpointsChanged, null); + } + + /// + /// Checks if the request is a document request (i.e. one that the module should handle) + /// + public bool IsDocumentRequest(string absPath) + { + var maybeDoc = true; + + // a document request should be + // /foo/bar/nil + // /foo/bar/nil/ + // where /foo is not a reserved path + + // if the path contains an extension + // then it cannot be a document request + var extension = Path.GetExtension(absPath); + if (maybeDoc && !extension.IsNullOrWhiteSpace()) { - _globalSettings = globalSettings.Value; - _routingSettings = routingSettings.Value; - _hostingEnvironment = hostingEnvironment; - _endpointDataSource = endpointDataSource; + maybeDoc = false; + } + + // at that point we have no extension + + // if the path is reserved then it cannot be a document request + if (maybeDoc && IsReservedPathOrUrl(absPath)) + { + maybeDoc = false; + } + + return maybeDoc; + } + + private void EndpointsChanged(object value) + { + lock (_routeLocker) + { + // try clearing each entry + foreach (var r in _routeChecks.Keys.ToList()) + { + _routeChecks.TryRemove(r, out _); + } + + // re-register after it has changed so we keep listening _endpointDataSource.GetChangeToken().RegisterChangeCallback(EndpointsChanged, null); } + } - private void EndpointsChanged(object value) + /// + /// Determines whether the specified URL is reserved or is inside a reserved path. + /// + /// The Path of the URL to check. + /// + /// true if the specified URL is reserved; otherwise, false. + /// + private bool IsReservedPathOrUrl(string absPath) + { + LazyInitializer.EnsureInitialized(ref _reservedList, ref _isInit, ref _initLocker, () => { - lock (_routeLocker) - { - // try clearing each entry - foreach (var r in _routeChecks.Keys.ToList()) - { - _routeChecks.TryRemove(r, out _); - } + // store references to strings to determine changes + var reservedPathsCache = _globalSettings.ReservedPaths; + var reservedUrlsCache = _globalSettings.ReservedUrls; - // re-register after it has changed so we keep listening - _endpointDataSource.GetChangeToken().RegisterChangeCallback(EndpointsChanged, null); + // add URLs and paths to a new list + var newReservedList = new HashSet(); + foreach (var reservedUrlTrimmed in NormalizePaths(reservedUrlsCache, false)) + { + newReservedList.Add(reservedUrlTrimmed); } + + foreach (var reservedPathTrimmed in NormalizePaths(reservedPathsCache, true)) + { + newReservedList.Add(reservedPathTrimmed); + } + + // use the new list from now on + return newReservedList; + }); + + // The URL should be cleaned up before checking: + // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' + // * We shouldn't be comparing the query at all + if (absPath.Contains('?')) + { + absPath = absPath.Split('?', StringSplitOptions.RemoveEmptyEntries)[0]; } - /// - /// Checks if the request is a document request (i.e. one that the module should handle) - /// - public bool IsDocumentRequest(string absPath) + if (absPath.Contains('.') == false) { - var maybeDoc = true; - - // a document request should be - // /foo/bar/nil - // /foo/bar/nil/ - // where /foo is not a reserved path - - // if the path contains an extension - // then it cannot be a document request - var extension = Path.GetExtension(absPath); - if (maybeDoc && !extension.IsNullOrWhiteSpace()) - { - maybeDoc = false; - } - - // at that point we have no extension - - // if the path is reserved then it cannot be a document request - if (maybeDoc && IsReservedPathOrUrl(absPath)) - { - maybeDoc = false; - } - - return maybeDoc; + absPath = absPath.EnsureEndsWith('/'); } - /// - /// Determines whether the specified URL is reserved or is inside a reserved path. - /// - /// The Path of the URL to check. - /// - /// true if the specified URL is reserved; otherwise, false. - /// - private bool IsReservedPathOrUrl(string absPath) + // return true if URL starts with an element of the reserved list + var isReserved = _reservedList?.Any(x => absPath.InvariantStartsWith(x)) ?? false; + + if (isReserved) { - LazyInitializer.EnsureInitialized(ref _reservedList, ref _isInit, ref _initLocker, () => + return true; + } + + // If configured, check if the current request matches a route, if so then it is reserved, + // else if not configured (default) proceed as normal since we assume the request is for an Umbraco content item. + var hasRoute = _routingSettings.TryMatchingEndpointsForAllPages && + _routeChecks.GetOrAdd(absPath, x => MatchesEndpoint(absPath)); + if (hasRoute) + { + return true; + } + + return false; + } + + private IEnumerable NormalizePaths(string paths, bool ensureTrailingSlash) => paths + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedPath => + { + var r = _hostingEnvironment.ToAbsolute(reservedPath).Trim().EnsureStartsWith('/'); + return ensureTrailingSlash + ? r.EnsureEndsWith('/') + : r; + }) + .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false); + + private bool MatchesEndpoint(string absPath) + { + // Borrowed and modified from https://stackoverflow.com/a/59550580 + + // Return a collection of Microsoft.AspNetCore.Http.Endpoint instances. + IEnumerable? routeEndpoints = _endpointDataSource.Endpoints + .OfType() + .Where(x => { - // store references to strings to determine changes - var reservedPathsCache = _globalSettings.ReservedPaths; - var reservedUrlsCache = _globalSettings.ReservedUrls; - - // add URLs and paths to a new list - var newReservedList = new HashSet(); - foreach (var reservedUrlTrimmed in NormalizePaths(reservedUrlsCache, false)) + // We don't want to include dynamic endpoints in this check since we would have no idea if that + // matches since they will probably match everything. + var isDynamic = x.Metadata.OfType().Any(y => y.IsDynamic); + if (isDynamic) { - newReservedList.Add(reservedUrlTrimmed); + return false; } - foreach (var reservedPathTrimmed in NormalizePaths(reservedPathsCache, true)) + // filter out matched endpoints that are suppressed + var isSuppressed = x.Metadata.OfType().FirstOrDefault()?.SuppressMatching == + true; + if (isSuppressed) { - newReservedList.Add(reservedPathTrimmed); + return false; } - // use the new list from now on - return newReservedList; + return true; }); - // The URL should be cleaned up before checking: - // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' - // * We shouldn't be comparing the query at all - if (absPath.Contains('?')) - { - absPath = absPath.Split('?', StringSplitOptions.RemoveEmptyEntries)[0]; - } + var routeValues = new RouteValueDictionary(); - if (absPath.Contains('.') == false) - { - absPath = absPath.EnsureEndsWith('/'); - } + // To get the matchedEndpoint of the provide url + RouteEndpoint? matchedEndpoint = routeEndpoints + .Where(e => e.RoutePattern.RawText != null) + .Where(e => new TemplateMatcher( + TemplateParser.Parse(e.RoutePattern.RawText!), + new RouteValueDictionary()) + .TryMatch(absPath, routeValues)) + .OrderBy(c => c.Order) + .FirstOrDefault(); - // return true if URL starts with an element of the reserved list - var isReserved = _reservedList?.Any(x => absPath.InvariantStartsWith(x)) ?? false; - - if (isReserved) - { - return true; - } - - // If configured, check if the current request matches a route, if so then it is reserved, - // else if not configured (default) proceed as normal since we assume the request is for an Umbraco content item. - var hasRoute = _routingSettings.TryMatchingEndpointsForAllPages && _routeChecks.GetOrAdd(absPath, x => MatchesEndpoint(absPath)); - if (hasRoute) - { - return true; - } - - return false; - } - - private IEnumerable NormalizePaths(string paths, bool ensureTrailingSlash) => paths - .Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim().ToLowerInvariant()) - .Where(x => x.IsNullOrWhiteSpace() == false) - .Select(reservedPath => - { - var r = _hostingEnvironment.ToAbsolute(reservedPath).Trim().EnsureStartsWith('/'); - return ensureTrailingSlash - ? r.EnsureEndsWith('/') - : r; - }) - .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false); - - private bool MatchesEndpoint(string absPath) - { - // Borrowed and modified from https://stackoverflow.com/a/59550580 - - // Return a collection of Microsoft.AspNetCore.Http.Endpoint instances. - IEnumerable? routeEndpoints = _endpointDataSource?.Endpoints - .OfType() - .Where(x => - { - // We don't want to include dynamic endpoints in this check since we would have no idea if that - // matches since they will probably match everything. - bool isDynamic = x.Metadata.OfType().Any(x => x.IsDynamic); - if (isDynamic) - { - return false; - } - - // filter out matched endpoints that are suppressed - var isSuppressed = x.Metadata.OfType().FirstOrDefault()?.SuppressMatching == true; - if (isSuppressed) - { - return false; - } - - return true; - }); - - var routeValues = new RouteValueDictionary(); - - // To get the matchedEndpoint of the provide url - RouteEndpoint? matchedEndpoint = routeEndpoints? - .Where(e => e.RoutePattern.RawText != null) - .Where(e => new TemplateMatcher( - TemplateParser.Parse(e.RoutePattern.RawText!), - new RouteValueDictionary()) - .TryMatch(absPath, routeValues)) - .OrderBy(c => c.Order) - .FirstOrDefault(); - - return matchedEndpoint != null; - } + return matchedEndpoint != null; } } diff --git a/src/Umbraco.Web.Common/Routing/UmbracoRequestOptions.cs b/src/Umbraco.Web.Common/Routing/UmbracoRequestOptions.cs index 2b27970cd6..9269f87026 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoRequestOptions.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoRequestOptions.cs @@ -1,14 +1,12 @@ -using System; using Microsoft.AspNetCore.Http; -namespace Umbraco.Cms.Web.Common.Routing +namespace Umbraco.Cms.Web.Common.Routing; + +public class UmbracoRequestOptions { - public class UmbracoRequestOptions - { - /// - /// Gets the delegate that checks if we're gonna handle a request as a client-side request - /// this returns true by default and can be overwritten in Startup.cs - /// - public Func HandleAsServerSideRequest { get; set; } = x => false; - } + /// + /// Gets the delegate that checks if we're gonna handle a request as a client-side request + /// this returns true by default and can be overwritten in Startup.cs + /// + public Func HandleAsServerSideRequest { get; set; } = x => false; } diff --git a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs index d06f9bcd28..62613a4b88 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs @@ -1,61 +1,59 @@ -using System; using Microsoft.AspNetCore.Mvc.Controllers; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Controllers; -namespace Umbraco.Cms.Web.Common.Routing +namespace Umbraco.Cms.Web.Common.Routing; + +/// +/// Represents the data required to route to a specific controller/action during an Umbraco request +/// +public class UmbracoRouteValues { /// - /// Represents the data required to route to a specific controller/action during an Umbraco request + /// The default action name /// - public class UmbracoRouteValues + public const string DefaultActionName = nameof(RenderController.Index); + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRouteValues( + IPublishedRequest publishedRequest, + ControllerActionDescriptor controllerActionDescriptor, + string? templateName = null) { - /// - /// The default action name - /// - public const string DefaultActionName = nameof(RenderController.Index); - - /// - /// Initializes a new instance of the class. - /// - public UmbracoRouteValues( - IPublishedRequest publishedRequest, - ControllerActionDescriptor controllerActionDescriptor, - string? templateName = null) - { - PublishedRequest = publishedRequest; - ControllerActionDescriptor = controllerActionDescriptor; - TemplateName = templateName; - } - - /// - /// Gets the controller name - /// - public string ControllerName => ControllerActionDescriptor.ControllerName; - - /// - /// Gets the action name - /// - public string ActionName => ControllerActionDescriptor.ActionName; - - /// - /// Gets the template name - /// - public string? TemplateName { get; } - - /// - /// Gets the controller type - /// - public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo; - - /// - /// Gets the Controller descriptor found for routing to - /// - public ControllerActionDescriptor ControllerActionDescriptor { get; } - - /// - /// Gets the - /// - public IPublishedRequest PublishedRequest { get; } + PublishedRequest = publishedRequest; + ControllerActionDescriptor = controllerActionDescriptor; + TemplateName = templateName; } + + /// + /// Gets the controller name + /// + public string ControllerName => ControllerActionDescriptor.ControllerName; + + /// + /// Gets the action name + /// + public string ActionName => ControllerActionDescriptor.ActionName; + + /// + /// Gets the template name + /// + public string? TemplateName { get; } + + /// + /// Gets the controller type + /// + public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo; + + /// + /// Gets the Controller descriptor found for routing to + /// + public ControllerActionDescriptor ControllerActionDescriptor { get; } + + /// + /// Gets the + /// + public IPublishedRequest PublishedRequest { get; } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeHelperAccessor.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeHelperAccessor.cs index b1dc202a37..299be83eae 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeHelperAccessor.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeHelperAccessor.cs @@ -1,29 +1,28 @@ -using System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Smidge; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +// work around for SmidgeHelper being request/scope lifetime +public sealed class SmidgeHelperAccessor { - // work around for SmidgeHelper being request/scope lifetime - public sealed class SmidgeHelperAccessor + private readonly IHttpContextAccessor _httpContextAccessor; + + public SmidgeHelperAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; + + public SmidgeHelper SmidgeHelper { - private readonly IHttpContextAccessor _httpContextAccessor; - - public SmidgeHelperAccessor(IHttpContextAccessor httpContextAccessor) + get { - _httpContextAccessor = httpContextAccessor; - } - - public SmidgeHelper SmidgeHelper - { - get + HttpContext? httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) { - var httpContext = _httpContextAccessor.HttpContext; - if (httpContext == null) - throw new InvalidOperationException($"Cannot get a {nameof(SmidgeHelper)} instance since there is no current http request"); - return httpContext.RequestServices.GetService()!; + throw new InvalidOperationException( + $"Cannot get a {nameof(SmidgeHelper)} instance since there is no current http request"); } + + return httpContext.RequestServices.GetService()!; } } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeNuglifyJs.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeNuglifyJs.cs index 74c298a4f5..e10ae0790b 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeNuglifyJs.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeNuglifyJs.cs @@ -1,29 +1,29 @@ +using NUglify.JavaScript; using Smidge; using Smidge.Nuglify; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +/// +/// Custom Nuglify Js pre-process to specify custom nuglify options without changing the global defaults +/// +public class SmidgeNuglifyJs : NuglifyJs { - /// - /// Custom Nuglify Js pre-process to specify custom nuglify options without changing the global defaults - /// - public class SmidgeNuglifyJs : NuglifyJs + public SmidgeNuglifyJs(NuglifySettings settings, ISourceMapDeclaration sourceMapDeclaration, IRequestHelper requestHelper) + : base(GetSettings(settings), sourceMapDeclaration, requestHelper) { - public SmidgeNuglifyJs(NuglifySettings settings, ISourceMapDeclaration sourceMapDeclaration, IRequestHelper requestHelper) - : base(GetSettings(settings), sourceMapDeclaration, requestHelper) - { - } + } - private static NuglifySettings GetSettings(NuglifySettings defaultSettings) - { - var nuglifyCodeSettings = defaultSettings.JsCodeSettings.CodeSettings.Clone(); + private static NuglifySettings GetSettings(NuglifySettings defaultSettings) + { + CodeSettings? nuglifyCodeSettings = defaultSettings.JsCodeSettings.CodeSettings.Clone(); - // Don't rename locals, this will kill a lot of angular stuff because we aren't correctly coding our - // angular injection to handle minification correctly which requires declaring string named versions of all - // dependencies injected (which is a pain). So we just turn this option off. - nuglifyCodeSettings.LocalRenaming = NUglify.JavaScript.LocalRenaming.KeepAll; - nuglifyCodeSettings.PreserveFunctionNames = true; + // Don't rename locals, this will kill a lot of angular stuff because we aren't correctly coding our + // angular injection to handle minification correctly which requires declaring string named versions of all + // dependencies injected (which is a pain). So we just turn this option off. + nuglifyCodeSettings.LocalRenaming = LocalRenaming.KeepAll; + nuglifyCodeSettings.PreserveFunctionNames = true; - return new NuglifySettings(new NuglifyCodeSettings(nuglifyCodeSettings), defaultSettings.CssCodeSettings); - } + return new NuglifySettings(new NuglifyCodeSettings(nuglifyCodeSettings), defaultSettings.CssCodeSettings); } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs index d054a0c036..37701928c6 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs @@ -2,20 +2,21 @@ using Microsoft.Extensions.Options; using Smidge.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +public class SmidgeOptionsSetup : IConfigureOptions { - public class SmidgeOptionsSetup : IConfigureOptions - { - private readonly IOptions _runtimeMinificatinoSettings; + private readonly IOptions _runtimeMinificatinoSettings; - public SmidgeOptionsSetup(IOptions runtimeMinificatinoSettings) - => _runtimeMinificatinoSettings = runtimeMinificatinoSettings; + public SmidgeOptionsSetup(IOptions runtimeMinificatinoSettings) + => _runtimeMinificatinoSettings = runtimeMinificatinoSettings; - /// - /// Configures Smidge to use in-memory caching if configured that way or if certain cache busters are used - /// - /// - public void Configure(SmidgeOptions options) - => options.CacheOptions.UseInMemoryCache = _runtimeMinificatinoSettings.Value.UseInMemoryCache || _runtimeMinificatinoSettings.Value.CacheBuster == RuntimeMinificationCacheBuster.Timestamp; - } + /// + /// Configures Smidge to use in-memory caching if configured that way or if certain cache busters are used + /// + /// + public void Configure(SmidgeOptions options) + => options.CacheOptions.UseInMemoryCache = _runtimeMinificatinoSettings.Value.UseInMemoryCache || + _runtimeMinificatinoSettings.Value.CacheBuster == + RuntimeMinificationCacheBuster.Timestamp; } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs index 4313e0e359..dd055a10c5 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs @@ -1,78 +1,73 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Smidge; using Smidge.Models; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +public class SmidgeRequestHelper : IRequestHelper { - public class SmidgeRequestHelper : IRequestHelper + private readonly RequestHelper _wrappedRequestHelper; + + public SmidgeRequestHelper(IWebsiteInfo siteInfo) => _wrappedRequestHelper = new RequestHelper(siteInfo); + + /// + public string Content(string path) => _wrappedRequestHelper.Content(path); + + /// + public string Content(IWebFile file) => _wrappedRequestHelper.Content(file); + + /// + public bool IsExternalRequestPath(string path) => _wrappedRequestHelper.IsExternalRequestPath(path); + + /// + /// Overrides the default order of compression from Smidge, since Brotli is super slow (~10 seconds for backoffice.js) + /// + /// + /// + public CompressionType GetClientCompression(IDictionary headers) { - private RequestHelper _wrappedRequestHelper; + CompressionType type = CompressionType.None; - public SmidgeRequestHelper(IWebsiteInfo siteInfo) + if (headers is not IHeaderDictionary headerDictionary) { - _wrappedRequestHelper = new RequestHelper(siteInfo); + headerDictionary = new HeaderDictionary(headers.Count); + foreach ((var key, StringValues stringValues) in headers) + { + headerDictionary[key] = stringValues; + } } - /// - public string Content(string path) => _wrappedRequestHelper.Content(path); - - /// - public string Content(IWebFile file) => _wrappedRequestHelper.Content(file); - - /// - public bool IsExternalRequestPath(string path) => _wrappedRequestHelper.IsExternalRequestPath(path); - - /// - /// Overrides the default order of compression from Smidge, since Brotli is super slow (~10 seconds for backoffice.js) - /// - /// - /// - public CompressionType GetClientCompression(IDictionary headers) + var acceptEncoding = headerDictionary.GetCommaSeparatedValues(HeaderNames.AcceptEncoding); + if (acceptEncoding.Length > 0) { - var type = CompressionType.None; - - if (headers is not IHeaderDictionary headerDictionary) + // Prefer in order: GZip, Deflate, Brotli. + for (var i = 0; i < acceptEncoding.Length; i++) { - headerDictionary = new HeaderDictionary(headers.Count); - foreach ((var key, StringValues stringValues) in headers) + var encoding = acceptEncoding[i].Trim(); + + var parsed = CompressionType.Parse(encoding); + + // Not pack200-gzip. + if (parsed == CompressionType.GZip) { - headerDictionary[key] = stringValues; + return CompressionType.GZip; + } + + if (parsed == CompressionType.Deflate) + { + type = CompressionType.Deflate; + } + + // Brotli is typically last in the accept encoding header. + if (type != CompressionType.Deflate && parsed == CompressionType.Brotli) + { + type = CompressionType.Brotli; } } - - var acceptEncoding = headerDictionary.GetCommaSeparatedValues(HeaderNames.AcceptEncoding); - if (acceptEncoding.Length > 0) - { - // Prefer in order: GZip, Deflate, Brotli. - for (var i = 0; i < acceptEncoding.Length; i++) - { - var encoding = acceptEncoding[i].Trim(); - - CompressionType parsed = CompressionType.Parse(encoding); - - // Not pack200-gzip. - if (parsed == CompressionType.GZip) - { - return CompressionType.GZip; - } - - if (parsed == CompressionType.Deflate) - { - type = CompressionType.Deflate; - } - - // Brotli is typically last in the accept encoding header. - if (type != CompressionType.Deflate && parsed == CompressionType.Brotli) - { - type = CompressionType.Brotli; - } - } - } - - return type; } + + return type; } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index 241a6e0ba0..362910e7e4 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Smidge; using Smidge.Cache; @@ -17,162 +13,172 @@ using Umbraco.Cms.Core.WebAssets; using CssFile = Smidge.Models.CssFile; using JavaScriptFile = Smidge.Models.JavaScriptFile; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +public class SmidgeRuntimeMinifier : IRuntimeMinifier { - public class SmidgeRuntimeMinifier : IRuntimeMinifier + private readonly IBundleManager _bundles; + private readonly CacheBusterResolver _cacheBusterResolver; + private readonly Type _cacheBusterType; + private readonly IConfigManipulator _configManipulator; + private readonly Lazy _cssMinPipeline; + private readonly Lazy _cssNonOptimizedPipeline; + private readonly Lazy _cssOptimizedPipeline; + private readonly IHostingEnvironment _hostingEnvironment; + + // used only for minifying in MinifyAsync not for an actual pipeline + private readonly Lazy _jsMinPipeline; + private readonly Lazy _jsNonOptimizedPipeline; + + // default pipelines for processing js/css files for the back office + private readonly Lazy _jsOptimizedPipeline; + private readonly SmidgeHelperAccessor _smidge; + private ICacheBuster? _cacheBuster; + + public SmidgeRuntimeMinifier( + IBundleManager bundles, + SmidgeHelperAccessor smidge, + IHostingEnvironment hostingEnvironment, + IConfigManipulator configManipulator, + IOptions runtimeMinificationSettings, + CacheBusterResolver cacheBusterResolver) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IConfigManipulator _configManipulator; - private readonly CacheBusterResolver _cacheBusterResolver; - private readonly IBundleManager _bundles; - private readonly SmidgeHelperAccessor _smidge; + _bundles = bundles; + _smidge = smidge; + _hostingEnvironment = hostingEnvironment; + _configManipulator = configManipulator; + _cacheBusterResolver = cacheBusterResolver; + _jsMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(JsMinifier))); + _cssMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(NuglifyCss))); - // used only for minifying in MinifyAsync not for an actual pipeline - private readonly Lazy _jsMinPipeline; - private readonly Lazy _cssMinPipeline; - - // default pipelines for processing js/css files for the back office - private readonly Lazy _jsOptimizedPipeline; - private readonly Lazy _jsNonOptimizedPipeline; - private readonly Lazy _cssOptimizedPipeline; - private readonly Lazy _cssNonOptimizedPipeline; - private ICacheBuster? _cacheBuster; - private readonly Type _cacheBusterType; - - public SmidgeRuntimeMinifier( - IBundleManager bundles, - SmidgeHelperAccessor smidge, - IHostingEnvironment hostingEnvironment, - IConfigManipulator configManipulator, - IOptions runtimeMinificationSettings, - CacheBusterResolver cacheBusterResolver) + // replace the default JsMinifier with NuglifyJs and CssMinifier with NuglifyCss in the default pipelines + // for use with our bundles only (not modifying global options) + _jsOptimizedPipeline = new Lazy(() => + bundles.PipelineFactory.DefaultJs().Replace(_bundles.PipelineFactory)); + _jsNonOptimizedPipeline = new Lazy(() => { - _bundles = bundles; - _smidge = smidge; - _hostingEnvironment = hostingEnvironment; - _configManipulator = configManipulator; - _cacheBusterResolver = cacheBusterResolver; - _jsMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(JsMinifier))); - _cssMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(NuglifyCss))); + PreProcessPipeline defaultJs = bundles.PipelineFactory.DefaultJs(); - // replace the default JsMinifier with NuglifyJs and CssMinifier with NuglifyCss in the default pipelines - // for use with our bundles only (not modifying global options) - _jsOptimizedPipeline = new Lazy(() => bundles.PipelineFactory.DefaultJs().Replace(_bundles.PipelineFactory)); - _jsNonOptimizedPipeline = new Lazy(() => - { - PreProcessPipeline defaultJs = bundles.PipelineFactory.DefaultJs(); - // remove minification from this pipeline - defaultJs.Processors.RemoveAll(x => x is JsMinifier); - return defaultJs; - }); - _cssOptimizedPipeline = new Lazy(() => bundles.PipelineFactory.DefaultCss().Replace(_bundles.PipelineFactory)); - _cssNonOptimizedPipeline = new Lazy(() => - { - PreProcessPipeline defaultCss = bundles.PipelineFactory.DefaultCss(); - // remove minification from this pipeline - defaultCss.Processors.RemoveAll(x => x is CssMinifier); - return defaultCss; - }); + // remove minification from this pipeline + defaultJs.Processors.RemoveAll(x => x is JsMinifier); + return defaultJs; + }); + _cssOptimizedPipeline = new Lazy(() => + bundles.PipelineFactory.DefaultCss().Replace(_bundles.PipelineFactory)); + _cssNonOptimizedPipeline = new Lazy(() => + { + PreProcessPipeline defaultCss = bundles.PipelineFactory.DefaultCss(); - Type cacheBusterType = runtimeMinificationSettings.Value.CacheBuster switch - { - RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster), - RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster), - RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster), - _ => throw new NotImplementedException() - }; + // remove minification from this pipeline + defaultCss.Processors.RemoveAll(x => x is CssMinifier); + return defaultCss; + }); - _cacheBusterType = cacheBusterType; + Type cacheBusterType = runtimeMinificationSettings.Value.CacheBuster switch + { + RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster), + RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster), + RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster), + _ => throw new NotImplementedException(), + }; + + _cacheBusterType = cacheBusterType; + } + + public string CacheBuster => (_cacheBuster ??= _cacheBusterResolver.GetCacheBuster(_cacheBusterType)).GetValue(); + + // only issue with creating bundles like this is that we don't have full control over the bundle options, though that could + public void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths) + { + if (filePaths?.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")) ?? false) + { + throw new InvalidOperationException("All file paths must be absolute"); } - public string CacheBuster => (_cacheBuster ??= _cacheBusterResolver.GetCacheBuster(_cacheBusterType)).GetValue(); - - // only issue with creating bundles like this is that we don't have full control over the bundle options, though that could - public void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths) + if (_bundles.Exists(bundleName)) { - if (filePaths?.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")) ?? false) - { - throw new InvalidOperationException("All file paths must be absolute"); - } - - if (_bundles.Exists(bundleName)) - { - throw new InvalidOperationException($"The bundle name {bundleName} already exists"); - } - - PreProcessPipeline pipeline = bundleOptions.OptimizeOutput - ? _cssOptimizedPipeline.Value - : _cssNonOptimizedPipeline.Value; - - Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Css, filePaths); - bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions)); + throw new InvalidOperationException($"The bundle name {bundleName} already exists"); } - public async Task RenderCssHereAsync(string bundleName) => (await _smidge.SmidgeHelper.CssHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString(); + PreProcessPipeline pipeline = bundleOptions.OptimizeOutput + ? _cssOptimizedPipeline.Value + : _cssNonOptimizedPipeline.Value; - public void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths) + Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Css, filePaths); + bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions)); + } + + public async Task RenderCssHereAsync(string bundleName) => + (await _smidge.SmidgeHelper.CssHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString(); + + public void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths) + { + if (filePaths?.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")) ?? false) { - if (filePaths?.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")) ?? false) - { - throw new InvalidOperationException("All file paths must be absolute"); - } - - if (_bundles.Exists(bundleName)) - { - throw new InvalidOperationException($"The bundle name {bundleName} already exists"); - } - - PreProcessPipeline pipeline = bundleOptions.OptimizeOutput - ? _jsOptimizedPipeline.Value - : _jsNonOptimizedPipeline.Value; - - Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Js, filePaths); - bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions)); + throw new InvalidOperationException("All file paths must be absolute"); } - private BundleEnvironmentOptions ConfigureBundleEnvironmentOptions(BundlingOptions bundleOptions) + if (_bundles.Exists(bundleName)) { - var bundleEnvironmentOptions = new BundleEnvironmentOptions(); - // auto-invalidate bundle if files change in debug - bundleEnvironmentOptions.DebugOptions.FileWatchOptions.Enabled = true; - // set cache busters - bundleEnvironmentOptions.DebugOptions.SetCacheBusterType(_cacheBusterType); - bundleEnvironmentOptions.ProductionOptions.SetCacheBusterType(_cacheBusterType); - // config if the files should be combined - bundleEnvironmentOptions.ProductionOptions.ProcessAsCompositeFile = bundleOptions.EnabledCompositeFiles; - - return bundleEnvironmentOptions; + throw new InvalidOperationException($"The bundle name {bundleName} already exists"); } - public async Task RenderJsHereAsync(string bundleName) => (await _smidge.SmidgeHelper.JsHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString(); + PreProcessPipeline pipeline = bundleOptions.OptimizeOutput + ? _jsOptimizedPipeline.Value + : _jsNonOptimizedPipeline.Value; - public async Task> GetJsAssetPathsAsync(string bundleName) => await _smidge.SmidgeHelper.GenerateJsUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode); + Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Js, filePaths); + bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions)); + } - public async Task> GetCssAssetPathsAsync(string bundleName) => await _smidge.SmidgeHelper.GenerateCssUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode); + public async Task RenderJsHereAsync(string bundleName) => + (await _smidge.SmidgeHelper.JsHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString(); - /// - public async Task MinifyAsync(string? fileContent, AssetType assetType) + public async Task> GetJsAssetPathsAsync(string bundleName) => + await _smidge.SmidgeHelper.GenerateJsUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode); + + public async Task> GetCssAssetPathsAsync(string bundleName) => + await _smidge.SmidgeHelper.GenerateCssUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode); + + /// + public async Task MinifyAsync(string? fileContent, AssetType assetType) + { + switch (assetType) { - switch (assetType) - { - case AssetType.Javascript: - return await _jsMinPipeline.Value - .ProcessAsync( - new FileProcessContext(fileContent, new JavaScriptFile(), BundleContext.CreateEmpty(CacheBuster))); - case AssetType.Css: - return await _cssMinPipeline.Value - .ProcessAsync(new FileProcessContext(fileContent, new CssFile(), BundleContext.CreateEmpty(CacheBuster))); - default: - throw new NotSupportedException("Unexpected AssetType"); - } - } - - /// - [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] - public void Reset() - { - var version = DateTime.UtcNow.Ticks.ToString(); - _configManipulator.SaveConfigValue(Cms.Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); + case AssetType.Javascript: + return await _jsMinPipeline.Value + .ProcessAsync( + new FileProcessContext(fileContent, new JavaScriptFile(), BundleContext.CreateEmpty(CacheBuster))); + case AssetType.Css: + return await _cssMinPipeline.Value + .ProcessAsync(new FileProcessContext(fileContent, new CssFile(), BundleContext.CreateEmpty(CacheBuster))); + default: + throw new NotSupportedException("Unexpected AssetType"); } } + + /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] + public void Reset() + { + var version = DateTime.UtcNow.Ticks.ToString(); + _configManipulator.SaveConfigValue(Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version); + } + + private BundleEnvironmentOptions ConfigureBundleEnvironmentOptions(BundlingOptions bundleOptions) + { + var bundleEnvironmentOptions = new BundleEnvironmentOptions(); + + // auto-invalidate bundle if files change in debug + bundleEnvironmentOptions.DebugOptions.FileWatchOptions.Enabled = true; + + // set cache busters + bundleEnvironmentOptions.DebugOptions.SetCacheBusterType(_cacheBusterType); + bundleEnvironmentOptions.ProductionOptions.SetCacheBusterType(_cacheBusterType); + + // config if the files should be combined + bundleEnvironmentOptions.ProductionOptions.ProcessAsCompositeFile = bundleOptions.EnabledCompositeFiles; + + return bundleEnvironmentOptions; + } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs index b3d114e103..c8b58f1518 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs @@ -1,5 +1,3 @@ -using System; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Smidge; using Smidge.Cache; @@ -7,71 +5,77 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.RuntimeMinification +namespace Umbraco.Cms.Web.Common.RuntimeMinification; + +/// +/// Constructs a cache buster string with sensible defaults. +/// +/// +/// +/// Had planned on handling all of this in SmidgeRuntimeMinifier, but that only handles some urls. +/// +/// +/// A lot of the work is delegated e.g. to +/// which doesn't care about the cache buster string in our classes only the value returned by the resolved +/// ICacheBuster. +/// +/// +/// Then I thought fine I'll just use a IConfigureOptions to tweak upstream , but +/// that only cares about the +/// class we instantiate and pass through in +/// +/// +/// +/// So here we are, create our own to ensure we cache bust in a reasonable fashion. +/// +///

+/// +/// Note that this class makes some other bits of code pretty redundant e.g. +/// will +/// concatenate version with CacheBuster value and hash again, but there's no real harm so can think about that +/// later. +/// +///
+internal class UmbracoSmidgeConfigCacheBuster : ICacheBuster { - /// - /// Constructs a cache buster string with sensible defaults. - /// - /// - /// - /// Had planned on handling all of this in SmidgeRuntimeMinifier, but that only handles some urls. - /// - /// - /// A lot of the work is delegated e.g. to - /// which doesn't care about the cache buster string in our classes only the value returned by the resolved ICacheBuster. - /// - /// - /// Then I thought fine I'll just use a IConfigureOptions to tweak upstream , but that only cares about the - /// class we instantiate and pass through in - /// - /// - /// So here we are, create our own to ensure we cache bust in a reasonable fashion. - /// - ///

- /// - /// Note that this class makes some other bits of code pretty redundant e.g. will - /// concatenate version with CacheBuster value and hash again, but there's no real harm so can think about that later. - /// - ///
- internal class UmbracoSmidgeConfigCacheBuster : ICacheBuster + private readonly IEntryAssemblyMetadata _entryAssemblyMetadata; + private readonly IOptions _runtimeMinificationSettings; + private readonly IUmbracoVersion _umbracoVersion; + + private string? _cacheBusterValue; + + public UmbracoSmidgeConfigCacheBuster( + IOptions runtimeMinificationSettings, + IUmbracoVersion umbracoVersion, + IEntryAssemblyMetadata entryAssemblyMetadata) { - private readonly IOptions _runtimeMinificationSettings; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IEntryAssemblyMetadata _entryAssemblyMetadata; + _runtimeMinificationSettings = runtimeMinificationSettings ?? + throw new ArgumentNullException(nameof(runtimeMinificationSettings)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _entryAssemblyMetadata = + entryAssemblyMetadata ?? throw new ArgumentNullException(nameof(entryAssemblyMetadata)); + } - private string? _cacheBusterValue; - - public UmbracoSmidgeConfigCacheBuster( - IOptions runtimeMinificationSettings, - IUmbracoVersion umbracoVersion, - IEntryAssemblyMetadata entryAssemblyMetadata) + private string CacheBusterValue + { + get { - _runtimeMinificationSettings = runtimeMinificationSettings ?? throw new ArgumentNullException(nameof(runtimeMinificationSettings)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _entryAssemblyMetadata = entryAssemblyMetadata ?? throw new ArgumentNullException(nameof(entryAssemblyMetadata)); - } - - private string CacheBusterValue - { - get + if (_cacheBusterValue != null) { - if (_cacheBusterValue != null) - { - return _cacheBusterValue; - } - - // Assembly Name adds a bit of uniqueness across sites when version missing from config. - // Adds a bit of security through obscurity that was asked for in standup. - var prefix = _runtimeMinificationSettings.Value.Version ?? _entryAssemblyMetadata.Name ?? string.Empty; - var umbracoVersion = _umbracoVersion.SemanticVersion.ToString(); - var downstreamVersion = _entryAssemblyMetadata.InformationalVersion; - - _cacheBusterValue = $"{prefix}_{umbracoVersion}_{downstreamVersion}".GenerateHash(); - return _cacheBusterValue; } - } - public string GetValue() => CacheBusterValue; + // Assembly Name adds a bit of uniqueness across sites when version missing from config. + // Adds a bit of security through obscurity that was asked for in standup. + var prefix = _runtimeMinificationSettings.Value.Version ?? _entryAssemblyMetadata.Name; + var umbracoVersion = _umbracoVersion.SemanticVersion.ToString(); + var downstreamVersion = _entryAssemblyMetadata.InformationalVersion; + + _cacheBusterValue = $"{prefix}_{umbracoVersion}_{downstreamVersion}".GenerateHash(); + + return _cacheBusterValue; + } } + + public string GetValue() => CacheBusterValue; } diff --git a/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs b/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs index bcb74aa286..63a684e6b3 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs @@ -2,21 +2,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class BackOfficeSecurityAccessor : IBackOfficeSecurityAccessor { - public class BackOfficeSecurityAccessor : IBackOfficeSecurityAccessor - { - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; - /// - /// Initializes a new instance of the class. - /// - public BackOfficeSecurityAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; + /// + /// Initializes a new instance of the class. + /// + public BackOfficeSecurityAccessor(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; - /// - /// Gets the current object. - /// - public IBackOfficeSecurity? BackOfficeSecurity - => _httpContextAccessor.HttpContext?.RequestServices?.GetService(); - } + /// + /// Gets the current object. + /// + // RequestServices can be null when testing, even though compiler says it can't + public IBackOfficeSecurity? BackOfficeSecurity + => _httpContextAccessor.HttpContext?.RequestServices?.GetService(); } diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 7928d122a7..09793081cf 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; using System.Security.Claims; using System.Security.Principal; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -15,225 +12,238 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class BackOfficeUserManager : UmbracoUserManager, + IBackOfficeUserManager { - public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager + private readonly IBackOfficeUserPasswordChecker _backOfficeUserPasswordChecker; + private readonly IEventAggregator _eventAggregator; + private readonly IHttpContextAccessor _httpContextAccessor; + + public BackOfficeUserManager( + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + BackOfficeErrorDescriber errors, + IServiceProvider services, + IHttpContextAccessor httpContextAccessor, + ILogger> logger, + IOptions passwordConfiguration, + IEventAggregator eventAggregator, + IBackOfficeUserPasswordChecker backOfficeUserPasswordChecker) + : base( + ipResolver, + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + errors, + services, + logger, + passwordConfiguration) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IEventAggregator _eventAggregator; - private readonly IBackOfficeUserPasswordChecker _backOfficeUserPasswordChecker; + _httpContextAccessor = httpContextAccessor; + _eventAggregator = eventAggregator; + _backOfficeUserPasswordChecker = backOfficeUserPasswordChecker; + } - public BackOfficeUserManager( - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - BackOfficeErrorDescriber errors, - IServiceProvider services, - IHttpContextAccessor httpContextAccessor, - ILogger> logger, - IOptions passwordConfiguration, - IEventAggregator eventAggregator, - IBackOfficeUserPasswordChecker backOfficeUserPasswordChecker) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) + /// + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's + /// locked out date + /// + /// The user + /// True if the user is locked out, else false + /// + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking + /// this for Umbraco we need to check both values + /// + public override async Task IsLockedOutAsync(BackOfficeIdentityUser user) + { + if (user == null) { - _httpContextAccessor = httpContextAccessor; - _eventAggregator = eventAggregator; - _backOfficeUserPasswordChecker = backOfficeUserPasswordChecker; + throw new ArgumentNullException(nameof(user)); } - /// - /// Override to allow checking the password via the if one is configured - /// - /// - /// - /// - /// - protected override async Task VerifyPasswordAsync( - IUserPasswordStore store, - BackOfficeIdentityUser user, - string password) + if (user.IsApproved == false) { - if (user.HasIdentity == false) - { - return PasswordVerificationResult.Failed; - } - - BackOfficeUserPasswordCheckerResult result = await _backOfficeUserPasswordChecker.CheckPasswordAsync(user, password); - - // if the result indicates to not fallback to the default, then return true if the credentials are valid - if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == BackOfficeUserPasswordCheckerResult.ValidCredentials - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - - return await base.VerifyPasswordAsync(store, user, password); + return true; } + return await base.IsLockedOutAsync(user); + } - /// - /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date - /// - /// The user - /// True if the user is locked out, else false - /// - /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values - /// - public override async Task IsLockedOutAsync(BackOfficeIdentityUser user) + public override async Task AccessFailedAsync(BackOfficeIdentityUser user) + { + IdentityResult result = await base.AccessFailedAsync(user); + + // Slightly confusing: this will return a Success if we successfully update the AccessFailed count + if (result.Succeeded) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.IsApproved == false) - { - return true; - } - - return await base.IsLockedOutAsync(user); + NotifyLoginFailed(_httpContextAccessor.HttpContext?.User, user.Id); } - public override async Task AccessFailedAsync(BackOfficeIdentityUser user) + return result; + } + + public override async Task ChangePasswordWithResetAsync(string userId, string token, string? newPassword) + { + IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); + if (result.Succeeded) { - IdentityResult result = await base.AccessFailedAsync(user); - - // Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Succeeded) - { - NotifyLoginFailed(_httpContextAccessor.HttpContext?.User, user.Id); - } - - return result; + NotifyPasswordReset(_httpContextAccessor.HttpContext?.User, userId); } - public override async Task ChangePasswordWithResetAsync(string userId, string token, string? newPassword) - { - IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); - if (result.Succeeded) - { - NotifyPasswordReset(_httpContextAccessor.HttpContext?.User, userId); - } + return result; + } - return result; + public override async Task ChangePasswordAsync(BackOfficeIdentityUser user, string? currentPassword, string? newPassword) + { + IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); + if (result.Succeeded) + { + NotifyPasswordChanged(_httpContextAccessor.HttpContext?.User, user.Id); } - public override async Task ChangePasswordAsync(BackOfficeIdentityUser user, string? currentPassword, string? newPassword) - { - IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); - if (result.Succeeded) - { - NotifyPasswordChanged(_httpContextAccessor.HttpContext?.User, user.Id); - } + return result; + } - return result; + public override async Task SetLockoutEndDateAsync( + BackOfficeIdentityUser user, + DateTimeOffset? lockoutEnd) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); } - /// - public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd) + IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); + + // The way we unlock is by setting the lockoutEnd date to the current datetime + if (result.Succeeded && lockoutEnd > DateTimeOffset.UtcNow) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + NotifyAccountLocked(_httpContextAccessor.HttpContext?.User, user.Id); + } + else + { + NotifyAccountUnlocked(_httpContextAccessor.HttpContext?.User, user.Id); - IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); - - // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Succeeded && lockoutEnd > DateTimeOffset.UtcNow) - { - NotifyAccountLocked(_httpContextAccessor.HttpContext?.User, user.Id); - } - else - { - NotifyAccountUnlocked(_httpContextAccessor.HttpContext?.User, user.Id); - - // Resets the login attempt fails back to 0 when unlock is clicked - await ResetAccessFailedCountAsync(user); - } - - return result; + // Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(user); } - /// - public override async Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) + return result; + } + + public override async Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) + { + IdentityResult result = await base.ResetAccessFailedCountAsync(user); + + // notify now that it's reset + NotifyResetAccessFailedCount(_httpContextAccessor.HttpContext?.User, user.Id); + + return result; + } + + public void NotifyForgotPasswordRequested(IPrincipal currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserForgotPasswordRequestedNotification(ip, userId, currentUserId)); + + public void NotifyForgotPasswordChanged(IPrincipal currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserForgotPasswordChangedNotification(ip, userId, currentUserId)); + + public SignOutSuccessResult NotifyLogoutSuccess(IPrincipal currentUser, string? userId) + { + UserLogoutSuccessNotification notification = Notify( + currentUser, + (currentUserId, ip) => new UserLogoutSuccessNotification(ip, userId, currentUserId)); + + return new SignOutSuccessResult { SignOutRedirectUrl = notification.SignOutRedirectUrl }; + } + + public void NotifyAccountLocked(IPrincipal? currentUser, string? userId) => Notify( + currentUser, + (currentUserId, ip) => new UserLockedNotification(ip, userId, currentUserId)); + + /// + /// Override to allow checking the password via the if one is configured + /// + /// + /// + /// + /// + protected override async Task VerifyPasswordAsync( + IUserPasswordStore store, + BackOfficeIdentityUser user, + string password) + { + if (user.HasIdentity == false) { - IdentityResult result = await base.ResetAccessFailedCountAsync(user); - - // notify now that it's reset - NotifyResetAccessFailedCount(_httpContextAccessor.HttpContext?.User, user.Id); - - return result; + return PasswordVerificationResult.Failed; } - private string GetCurrentUserId(IPrincipal? currentUser) + BackOfficeUserPasswordCheckerResult result = + await _backOfficeUserPasswordChecker.CheckPasswordAsync(user, password); + + // if the result indicates to not fallback to the default, then return true if the credentials are valid + if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) { - ClaimsIdentity? umbIdentity = currentUser?.GetUmbracoIdentity(); - var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserIdAsString; - return currentUserId; + return result == BackOfficeUserPasswordCheckerResult.ValidCredentials + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; } - public void NotifyAccountLocked(IPrincipal? currentUser, string? userId) => Notify(currentUser, - (currentUserId, ip) => new UserLockedNotification(ip, userId, currentUserId) - ); + return await base.VerifyPasswordAsync(store, user, password); + } - public void NotifyAccountUnlocked(IPrincipal? currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserUnlockedNotification(ip, userId, currentUserId) - ); + private string GetCurrentUserId(IPrincipal? currentUser) + { + ClaimsIdentity? umbIdentity = currentUser?.GetUmbracoIdentity(); + var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserIdAsString; + return currentUserId; + } - public void NotifyForgotPasswordRequested(IPrincipal currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserForgotPasswordRequestedNotification(ip, userId, currentUserId) - ); + public void NotifyAccountUnlocked(IPrincipal? currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserUnlockedNotification(ip, userId, currentUserId)); - public void NotifyForgotPasswordChanged(IPrincipal currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserForgotPasswordChangedNotification(ip, userId, currentUserId) - ); + public void NotifyLoginFailed(IPrincipal? currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserLoginFailedNotification(ip, userId, currentUserId)); - public void NotifyLoginFailed(IPrincipal? currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserLoginFailedNotification(ip, userId, currentUserId) - ); + public void NotifyLoginRequiresVerification(IPrincipal currentUser, string? userId) => Notify( + currentUser, + (currentUserId, ip) => new UserLoginRequiresVerificationNotification(ip, userId, currentUserId)); - public void NotifyLoginRequiresVerification(IPrincipal currentUser, string? userId) => Notify(currentUser, - (currentUserId, ip) => new UserLoginRequiresVerificationNotification(ip, userId, currentUserId) - ); + public void NotifyLoginSuccess(IPrincipal currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserLoginSuccessNotification(ip, userId, currentUserId)); - public void NotifyLoginSuccess(IPrincipal currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserLoginSuccessNotification(ip, userId, currentUserId) - ); + public void NotifyPasswordChanged(IPrincipal? currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserPasswordChangedNotification(ip, userId, currentUserId)); - public SignOutSuccessResult NotifyLogoutSuccess(IPrincipal currentUser, string? userId) - { - var notification = Notify(currentUser, - (currentUserId, ip) => new UserLogoutSuccessNotification(ip, userId, currentUserId) - ); + public void NotifyPasswordReset(IPrincipal? currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserPasswordResetNotification(ip, userId, currentUserId)); - return new SignOutSuccessResult { SignOutRedirectUrl = notification.SignOutRedirectUrl }; - } + public void NotifyResetAccessFailedCount(IPrincipal? currentUser, string userId) => Notify( + currentUser, + (currentUserId, ip) => new UserResetAccessFailedCountNotification(ip, userId, currentUserId)); - public void NotifyPasswordChanged(IPrincipal? currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserPasswordChangedNotification(ip, userId, currentUserId) - ); + private T Notify(IPrincipal? currentUser, Func createNotification) + where T : INotification + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); - public void NotifyPasswordReset(IPrincipal? currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserPasswordResetNotification(ip, userId, currentUserId) - ); - - public void NotifyResetAccessFailedCount(IPrincipal? currentUser, string userId) => Notify(currentUser, - (currentUserId, ip) => new UserResetAccessFailedCountNotification(ip, userId, currentUserId) - ); - - private T Notify(IPrincipal? currentUser, Func createNotification) where T : INotification - { - var currentUserId = GetCurrentUserId(currentUser); - var ip = IpResolver.GetCurrentRequestIpAddress(); - - var notification = createNotification(currentUserId, ip); - _eventAggregator.Publish(notification); - return notification; - } + T notification = createNotification(currentUserId, ip); + _eventAggregator.Publish(notification); + return notification; } } diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs index 8630197650..e1fa57557b 100644 --- a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs +++ b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs @@ -6,67 +6,64 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class BackOfficeSecurity : IBackOfficeSecurity { - public class BackOfficeSecurity : IBackOfficeSecurity + private readonly object _currentUserLock = new(); + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserService _userService; + private IUser? _currentUser; + + public BackOfficeSecurity( + IUserService userService, + IHttpContextAccessor httpContextAccessor) { - private readonly IUserService _userService; - private readonly IHttpContextAccessor _httpContextAccessor; + _userService = userService; + _httpContextAccessor = httpContextAccessor; + } - private readonly object _currentUserLock = new object(); - private IUser? _currentUser; - - public BackOfficeSecurity( - IUserService userService, - IHttpContextAccessor httpContextAccessor) + /// + public IUser? CurrentUser + { + get { - _userService = userService; - _httpContextAccessor = httpContextAccessor; - } - - /// - public IUser? CurrentUser - { - get + // only load it once per instance! (but make sure groups are loaded) + if (_currentUser == null) { - - //only load it once per instance! (but make sure groups are loaded) - if (_currentUser == null) + lock (_currentUserLock) { - lock (_currentUserLock) + // Check again + if (_currentUser == null) { - //Check again - if (_currentUser == null) + Attempt id = GetUserId(); + if (id.Success && id.Result is not null) { - Attempt id = GetUserId(); - if (id.Success && id.Result is not null) - { - _currentUser = id.Success ? _userService.GetUserById(id.Result.Value) : null; - } - + _currentUser = id.Success ? _userService.GetUserById(id.Result.Value) : null; } } } - - return _currentUser; } - } - /// - public Attempt GetUserId() - { - ClaimsIdentity? identity = _httpContextAccessor.HttpContext?.GetCurrentIdentity(); - return identity == null ? Attempt.Fail() : Attempt.Succeed(identity.GetId()); + return _currentUser; } - - /// - public bool IsAuthenticated() - { - HttpContext? httpContext = _httpContextAccessor.HttpContext; - return httpContext?.User != null && (httpContext.User.Identity?.IsAuthenticated ?? false) && httpContext.GetCurrentIdentity() != null; - } - - /// - public bool UserHasSectionAccess(string section, IUser user) => user.HasSectionAccess(section); } + + /// + public Attempt GetUserId() + { + ClaimsIdentity? identity = _httpContextAccessor.HttpContext?.GetCurrentIdentity(); + return identity == null ? Attempt.Fail() : Attempt.Succeed(identity.GetId()); + } + + /// + public bool IsAuthenticated() + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + return httpContext?.User != null && (httpContext.User.Identity?.IsAuthenticated ?? false) && + httpContext.GetCurrentIdentity() != null; + } + + /// + public bool UserHasSectionAccess(string section, IUser user) => user.HasSectionAccess(section); } diff --git a/src/Umbraco.Web.Common/Security/ConfigureFormOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureFormOptions.cs index 25427461d5..938830d5e3 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureFormOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureFormOptions.cs @@ -2,17 +2,18 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Security -{ - public class ConfigureFormOptions : IConfigureOptions - { - private readonly IOptions _runtimeSettings; +namespace Umbraco.Cms.Web.Common.Security; - public ConfigureFormOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; - public void Configure(FormOptions options) - { - // convert from KB to bytes - options.MultipartBodyLengthLimit = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : long.MaxValue; - } - } +public class ConfigureFormOptions : IConfigureOptions +{ + private readonly IOptions _runtimeSettings; + + public ConfigureFormOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; + + public void Configure(FormOptions options) => + + // convert from KB to bytes + options.MultipartBodyLengthLimit = _runtimeSettings.Value.MaxRequestLength.HasValue + ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 + : long.MaxValue; } diff --git a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs index 1c1460432f..fe29d86f51 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs @@ -1,24 +1,22 @@ -using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +[Obsolete( + "This class is obsolete, as this does not configure your Maximum request length, see https://our.umbraco.com/documentation/Reference/V9-Config/MaximumUploadSizeSettings/ for information about configuring maximum request length")] +public class ConfigureIISServerOptions : IConfigureOptions { - [Obsolete("This class is obsolete, as this does not configure your Maximum request length, see https://our.umbraco.com/documentation/Reference/V9-Config/MaximumUploadSizeSettings/ for information about configuring maximum request length")] - public class ConfigureIISServerOptions : IConfigureOptions - { - private readonly IOptions _runtimeSettings; + private readonly IOptions _runtimeSettings; - public ConfigureIISServerOptions(IOptions runtimeSettings) => - _runtimeSettings = runtimeSettings; + public ConfigureIISServerOptions(IOptions runtimeSettings) => + _runtimeSettings = runtimeSettings; - public void Configure(IISServerOptions options) - { - // convert from KB to bytes - options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue - ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 - : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. - } - } + public void Configure(IISServerOptions options) => + + // convert from KB to bytes + options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue + ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 + : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. } diff --git a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs index 59ec330700..f388be0391 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs @@ -2,17 +2,19 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Web.Common.Security -{ - public class ConfigureKestrelServerOptions : IConfigureOptions - { - private readonly IOptions _runtimeSettings; +namespace Umbraco.Cms.Web.Common.Security; - public ConfigureKestrelServerOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; - public void Configure(KestrelServerOptions options) - { - // convert from KB to bytes, 52428800 bytes (50 MB) is the same as in the IIS settings - options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : 52428800; - } - } +public class ConfigureKestrelServerOptions : IConfigureOptions +{ + private readonly IOptions _runtimeSettings; + + public ConfigureKestrelServerOptions(IOptions runtimeSettings) => + _runtimeSettings = runtimeSettings; + + public void Configure(KestrelServerOptions options) => + + // convert from KB to bytes, 52428800 bytes (50 MB) is the same as in the IIS settings + options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue + ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 + : 52428800; } diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index c4649611d3..1e0960fbc7 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -6,49 +5,47 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions { - public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions + private readonly IRuntimeState _runtimeState; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + + public ConfigureMemberCookieOptions(IRuntimeState runtimeState, UmbracoRequestPaths umbracoRequestPaths) { - private readonly IRuntimeState _runtimeState; - private readonly UmbracoRequestPaths _umbracoRequestPaths; + _runtimeState = runtimeState; + _umbracoRequestPaths = umbracoRequestPaths; + } - public ConfigureMemberCookieOptions(IRuntimeState runtimeState, UmbracoRequestPaths umbracoRequestPaths) + public void Configure(string name, CookieAuthenticationOptions options) + { + if (name == IdentityConstants.ApplicationScheme || name == IdentityConstants.ExternalScheme) { - _runtimeState = runtimeState; - _umbracoRequestPaths = umbracoRequestPaths; - } - - public void Configure(string name, CookieAuthenticationOptions options) - { - if (name == IdentityConstants.ApplicationScheme || name == IdentityConstants.ExternalScheme) - { - Configure(options); - } - } - - public void Configure(CookieAuthenticationOptions options) - { - // TODO: We may want/need to configure these further - - options.LoginPath = null; - options.AccessDeniedPath = null; - options.LogoutPath = null; - - options.CookieManager = new MemberCookieManager(_runtimeState, _umbracoRequestPaths); - - options.Events = new CookieAuthenticationEvents - { - OnSignedIn = ctx => - { - // occurs when sign in is successful and after the ticket is written to the outbound cookie - - // When we are signed in with the cookie, assign the principal to the current HttpContext - ctx.HttpContext.SetPrincipalForRequest(ctx.Principal); - - return Task.CompletedTask; - } - }; + Configure(options); } } + + public void Configure(CookieAuthenticationOptions options) + { + // TODO: We may want/need to configure these further + options.LoginPath = null; + options.AccessDeniedPath = null; + options.LogoutPath = null; + + options.CookieManager = new MemberCookieManager(_runtimeState, _umbracoRequestPaths); + + options.Events = new CookieAuthenticationEvents + { + OnSignedIn = ctx => + { + // occurs when sign in is successful and after the ticket is written to the outbound cookie + + // When we are signed in with the cookie, assign the principal to the current HttpContext + ctx.HttpContext.SetPrincipalForRequest(ctx.Principal); + + return Task.CompletedTask; + }, + }; + } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs index db82ff2b05..891925fe49 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs @@ -1,41 +1,41 @@ -using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public sealed class ConfigureMemberIdentityOptions : IConfigureOptions { + private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; + private readonly SecuritySettings _securitySettings; - public sealed class ConfigureMemberIdentityOptions : IConfigureOptions + public ConfigureMemberIdentityOptions( + IOptions memberPasswordConfiguration, + IOptions securitySettings) { - private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; - private readonly SecuritySettings _securitySettings; + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + _securitySettings = securitySettings.Value; + } - public ConfigureMemberIdentityOptions(IOptions memberPasswordConfiguration, IOptions securitySettings) - { - _memberPasswordConfiguration = memberPasswordConfiguration.Value; - _securitySettings = securitySettings.Value; - } + public void Configure(IdentityOptions options) + { + options.SignIn.RequireConfirmedAccount = true; // uses our custom IUserConfirmation + options.SignIn.RequireConfirmedEmail = false; // not implemented + options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented - public void Configure(IdentityOptions options) - { - options.SignIn.RequireConfirmedAccount = true; // uses our custom IUserConfirmation - options.SignIn.RequireConfirmedEmail = false; // not implemented - options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented + options.User.RequireUniqueEmail = true; - options.User.RequireUniqueEmail = true; + // Support validation of member names using Down-Level Logon Name format + options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters; - // Support validation of member names using Down-Level Logon Name format - options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters; + options.Lockout.AllowedForNewUsers = true; - options.Lockout.AllowedForNewUsers = true; - // TODO: Implement this - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + // TODO: Implement this + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); - options.Password.ConfigurePasswordOptions(_memberPasswordConfiguration); + options.Password.ConfigurePasswordOptions(_memberPasswordConfiguration); - options.Lockout.MaxFailedAccessAttempts = _memberPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; - } + options.Lockout.MaxFailedAccessAttempts = _memberPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs index 66cf97fd4c..23381dd34c 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs @@ -1,40 +1,36 @@ -using System; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class ConfigureSecurityStampOptions : IConfigureOptions { - public class ConfigureSecurityStampOptions : IConfigureOptions + /// + /// Configures security stamp options and ensures any custom claims + /// set on the identity are persisted to the new identity when it's refreshed. + /// + /// + public static void ConfigureOptions(SecurityStampValidatorOptions options) { - public void Configure(SecurityStampValidatorOptions options) - => ConfigureOptions(options); + options.ValidationInterval = TimeSpan.FromMinutes(30); - /// - /// Configures security stamp options and ensures any custom claims - /// set on the identity are persisted to the new identity when it's refreshed. - /// - /// - public static void ConfigureOptions(SecurityStampValidatorOptions options) + // When refreshing the principal, ensure custom claims that + // might have been set with an external identity continue + // to flow through to this new one. + options.OnRefreshingPrincipal = refreshingPrincipal => { - options.ValidationInterval = TimeSpan.FromMinutes(30); + ClaimsIdentity newIdentity = refreshingPrincipal.NewPrincipal.Identities.First(); + ClaimsIdentity currentIdentity = refreshingPrincipal.CurrentPrincipal.Identities.First(); - // When refreshing the principal, ensure custom claims that - // might have been set with an external identity continue - // to flow through to this new one. - options.OnRefreshingPrincipal = refreshingPrincipal => - { - ClaimsIdentity newIdentity = refreshingPrincipal.NewPrincipal.Identities.First(); - ClaimsIdentity currentIdentity = refreshingPrincipal.CurrentPrincipal.Identities.First(); + // Since this is refreshing an existing principal, we want to merge all claims. + newIdentity.MergeAllClaims(currentIdentity); - // Since this is refreshing an existing principal, we want to merge all claims. - newIdentity.MergeAllClaims(currentIdentity); - - return Task.CompletedTask; - }; - } + return Task.CompletedTask; + }; } + + public void Configure(SecurityStampValidatorOptions options) + => ConfigureOptions(options); } diff --git a/src/Umbraco.Web.Common/Security/EncryptionHelper.cs b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs index 8c9ebe2fff..1328d79e00 100644 --- a/src/Umbraco.Web.Common/Security/EncryptionHelper.cs +++ b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Net; using System.Web; using Microsoft.AspNetCore.DataProtection; @@ -11,126 +8,137 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Web.Common.Constants; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class EncryptionHelper { - public class EncryptionHelper + public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider) + => CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString); + + // TODO: Decide if these belong here... I don't think so since this all has to do with surface controller routes + // could also just be injected too.... + private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider) + => dataProtectionProvider.CreateProtector(nameof(EncryptionHelper)); + + public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider) + => CreateDataProtector(dataProtectionProvider).Protect(plainString); + + /// + /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets + /// submitted in a request for which + /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. + /// + public static string CreateEncryptedRouteString( + IDataProtectionProvider dataProtectionProvider, + string controllerName, + string controllerAction, + string area, + object? additionalRouteVals = null) { - // TODO: Decide if these belong here... I don't think so since this all has to do with surface controller routes - // could also just be injected too.... - - private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider) - => dataProtectionProvider.CreateProtector(nameof(EncryptionHelper)); - - public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider) - => CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString); - - public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider) - => CreateDataProtector(dataProtectionProvider).Protect(plainString); - - /// - /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which - /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. - /// - public static string CreateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string controllerName, string controllerAction, string area, object? additionalRouteVals = null) + if (dataProtectionProvider is null) { - if (dataProtectionProvider is null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } + throw new ArgumentNullException(nameof(dataProtectionProvider)); + } - if (string.IsNullOrEmpty(controllerName)) - { - throw new ArgumentException($"'{nameof(controllerName)}' cannot be null or empty.", nameof(controllerName)); - } + if (string.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException($"'{nameof(controllerName)}' cannot be null or empty.", nameof(controllerName)); + } - if (string.IsNullOrEmpty(controllerAction)) - { - throw new ArgumentException($"'{nameof(controllerAction)}' cannot be null or empty.", nameof(controllerAction)); - } + if (string.IsNullOrEmpty(controllerAction)) + { + throw new ArgumentException( + $"'{nameof(controllerAction)}' cannot be null or empty.", + nameof(controllerAction)); + } - if (area is null) - { - throw new ArgumentNullException(nameof(area)); - } + if (area is null) + { + throw new ArgumentNullException(nameof(area)); + } - // need to create a params string as Base64 to put into our hidden field to use during the routes - var surfaceRouteParams = $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}"; + // need to create a params string as Base64 to put into our hidden field to use during the routes + var surfaceRouteParams = + $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}"; - // checking if the additional route values is already a dictionary and convert to querystring - string? additionalRouteValsAsQuery; - if (additionalRouteVals != null) + // checking if the additional route values is already a dictionary and convert to querystring + string? additionalRouteValsAsQuery; + if (additionalRouteVals != null) + { + if (additionalRouteVals is Dictionary additionalRouteValsAsDictionary) { - if (additionalRouteVals is Dictionary additionalRouteValsAsDictionary) - { - additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString(); - } - else - { - additionalRouteValsAsQuery = additionalRouteVals.ToDictionary().ToQueryString(); - } + additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString(); } else { - additionalRouteValsAsQuery = null; + additionalRouteValsAsQuery = additionalRouteVals.ToDictionary().ToQueryString(); } - - if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false) - { - surfaceRouteParams += "&" + additionalRouteValsAsQuery; - } - - return Encrypt(surfaceRouteParams, dataProtectionProvider); } - - public static bool DecryptAndValidateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string encryptedString, [MaybeNullWhen(false)] out IDictionary parts) + else { - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - string decryptedString; - try - { - decryptedString = Decrypt(encryptedString, dataProtectionProvider); - } - catch (Exception) - { - StaticApplicationLogging.Logger.LogWarning("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); - parts = null; - return false; - } - - NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(decryptedString); - parts = new Dictionary(); - foreach (var key in parsedQueryString.AllKeys) - { - if (key is not null) - { - parts[key] = parsedQueryString[key]; - } - } - - // validate all required keys exist - // the controller - if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller)) - { - return false; - } - - // the action - if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action)) - { - return false; - } - - // the area - if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area)) - { - return false; - } - - return true; + additionalRouteValsAsQuery = null; } + + if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false) + { + surfaceRouteParams += "&" + additionalRouteValsAsQuery; + } + + return Encrypt(surfaceRouteParams, dataProtectionProvider); + } + + public static bool DecryptAndValidateEncryptedRouteString( + IDataProtectionProvider dataProtectionProvider, + string encryptedString, + [MaybeNullWhen(false)] out IDictionary parts) + { + if (dataProtectionProvider == null) + { + throw new ArgumentNullException(nameof(dataProtectionProvider)); + } + + string decryptedString; + try + { + decryptedString = Decrypt(encryptedString, dataProtectionProvider); + } + catch (Exception) + { + StaticApplicationLogging.Logger.LogWarning( + "A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); + parts = null; + return false; + } + + NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + parts = new Dictionary(); + foreach (var key in parsedQueryString.AllKeys) + { + if (key is not null) + { + parts[key] = parsedQueryString[key]; + } + } + + // validate all required keys exist + // the controller + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller)) + { + return false; + } + + // the action + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action)) + { + return false; + } + + // the area + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area)) + { + return false; + } + + return true; } } diff --git a/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs index fcbadd191c..8e674c8b9f 100644 --- a/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs @@ -1,27 +1,35 @@ -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.BackOffice.Security +namespace Umbraco.Cms.Web.BackOffice.Security; + +/// +/// A for the back office with a +/// +/// +public interface IBackOfficeSignInManager { - /// - /// A for the back office with a - /// - public interface IBackOfficeSignInManager - { - AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string? redirectUrl, string? userId = null); - Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false); - Task> GetExternalAuthenticationSchemesAsync(); - Task GetExternalLoginInfoAsync(string? expectedXsrf = null); - Task GetTwoFactorAuthenticationUserAsync(); - Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure); - Task SignOutAsync(); - Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string? authenticationMethod = null); - Task CreateUserPrincipalAsync(BackOfficeIdentityUser user); - Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient); - Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); - } + AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string? redirectUrl, string? userId = null); + + Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false); + + Task> GetExternalAuthenticationSchemesAsync(); + + Task GetExternalLoginInfoAsync(string? expectedXsrf = null); + + Task GetTwoFactorAuthenticationUserAsync(); + + Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure); + + Task SignOutAsync(); + + Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string? authenticationMethod = null); + + Task CreateUserPrincipalAsync(BackOfficeIdentityUser user); + + Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient); + + Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); } diff --git a/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs index 50beb1f65a..3d72c7d27e 100644 --- a/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs +++ b/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs @@ -1,26 +1,20 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +namespace Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Cms.Web.Common.Security +/// +/// Service to return instances +/// +public interface IMemberExternalLoginProviders { + /// + /// Get the for the specified scheme + /// + /// + /// + Task GetAsync(string authenticationType); /// - /// Service to return instances + /// Get all registered /// - public interface IMemberExternalLoginProviders - { - /// - /// Get the for the specified scheme - /// - /// - /// - Task GetAsync(string authenticationType); - - /// - /// Get all registered - /// - /// - Task> GetMemberProvidersAsync(); - } - + /// + Task> GetMemberProvidersAsync(); } diff --git a/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs index e10bc118be..71095a196b 100644 --- a/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs +++ b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public interface IMemberRoleManager { - public interface IMemberRoleManager - { - IEnumerable Roles { get; } - } + IEnumerable Roles { get; } } diff --git a/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs b/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs index 6eb67eb60b..a5a444bd06 100644 --- a/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs @@ -1,23 +1,27 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Security; +namespace Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Cms.Web.Common.Security +public interface IMemberSignInManager { - public interface IMemberSignInManager - { - // TODO: We could have a base interface for these to share with IBackOfficeSignInManager - Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure); - Task SignInAsync(MemberIdentityUser user, bool isPersistent, string? authenticationMethod = null); - Task SignOutAsync(); + // TODO: We could have a base interface for these to share with IBackOfficeSignInManager + Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure); - AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string? userId = null); - Task GetExternalLoginInfoAsync(string? expectedXsrf = null); - Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); - Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false); - Task GetTwoFactorAuthenticationUserAsync(); - Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient); - } + Task SignInAsync(MemberIdentityUser user, bool isPersistent, string? authenticationMethod = null); + + Task SignOutAsync(); + + AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string? userId = null); + + Task GetExternalLoginInfoAsync(string? expectedXsrf = null); + + Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); + + Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false); + + Task GetTwoFactorAuthenticationUserAsync(); + + Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient); } diff --git a/src/Umbraco.Web.Common/Security/MemberCookieManager.cs b/src/Umbraco.Web.Common/Security/MemberCookieManager.cs index bdc4306e55..01deb1af2f 100644 --- a/src/Umbraco.Web.Common/Security/MemberCookieManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberCookieManager.cs @@ -1,70 +1,68 @@ -using System; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// A custom cookie manager for members to ensure that cookie auth does not occur for any back office requests +/// +public class MemberCookieManager : ChunkingCookieManager, ICookieManager { - /// - /// A custom cookie manager for members to ensure that cookie auth does not occur for any back office requests - /// - public class MemberCookieManager : ChunkingCookieManager, ICookieManager + private readonly IRuntimeState _runtime; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + + public MemberCookieManager(IRuntimeState runtime, UmbracoRequestPaths umbracoRequestPaths) { - private readonly IRuntimeState _runtime; - private readonly UmbracoRequestPaths _umbracoRequestPaths; + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + _umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths)); + } - public MemberCookieManager(IRuntimeState runtime, UmbracoRequestPaths umbracoRequestPaths) + /// + /// Explicitly implement this so that we filter the request + /// + /// + string? ICookieManager.GetRequestCookie(HttpContext context, string key) + { + PathString absPath = context.Request.Path; + + return ShouldAuthenticateRequest(absPath) == false + + // Don't auth request, don't return a cookie + ? null + + // Return the default implementation + : GetRequestCookie(context, key); + } + + /// + /// Determines if we should authenticate the request + /// + /// true if the request should be authenticated + /// + /// We auth the request when it is not a back office request and when the runtime level is Run + /// + public bool ShouldAuthenticateRequest(string absPath) + { + // Do not authenticate the request if we are not running. + // Else this can cause problems especially if the members DB table needs upgrades + // because when authing, the member db table will be read and we'll get exceptions. + if (_runtime.Level != RuntimeLevel.Run) { - _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - _umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths)); + return false; } - /// - /// Determines if we should authenticate the request - /// - /// true if the request should be authenticated - /// - /// We auth the request when it is not a back office request and when the runtime level is Run - /// - public bool ShouldAuthenticateRequest(string absPath) + if (// check back office + _umbracoRequestPaths.IsBackOfficeRequest(absPath) + + // check installer + || _umbracoRequestPaths.IsInstallerRequest(absPath)) { - // Do not authenticate the request if we are not running. - // Else this can cause problems especially if the members DB table needs upgrades - // because when authing, the member db table will be read and we'll get exceptions. - if (_runtime.Level != RuntimeLevel.Run) - { - return false; - } - - if (// check back office - _umbracoRequestPaths.IsBackOfficeRequest(absPath) - - // check installer - || _umbracoRequestPaths.IsInstallerRequest(absPath)) - { - return false; - } - - return true; + return false; } - /// - /// Explicitly implement this so that we filter the request - /// - /// - string? ICookieManager.GetRequestCookie(HttpContext context, string key) - { - var absPath = context.Request.Path; - - return ShouldAuthenticateRequest(absPath) == false - - // Don't auth request, don't return a cookie - ? null - - // Return the default implementation - : GetRequestCookie(context, key); - } + return true; } } diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs index e146f7bc2c..4d0c8390a9 100644 --- a/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs @@ -1,36 +1,36 @@ -using System; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Web.Common.Security -{ - /// - /// An external login (OAuth) provider for the members - /// - public class MemberExternalLoginProvider : IEquatable - { - public MemberExternalLoginProvider( - string authenticationType, - IOptionsMonitor properties) - { - if (properties is null) - { - throw new ArgumentNullException(nameof(properties)); - } +namespace Umbraco.Cms.Web.Common.Security; - AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType)); - Options = properties.Get(authenticationType); +/// +/// An external login (OAuth) provider for the members +/// +public class MemberExternalLoginProvider : IEquatable +{ + public MemberExternalLoginProvider( + string authenticationType, + IOptionsMonitor properties) + { + if (properties is null) + { + throw new ArgumentNullException(nameof(properties)); } - /// - /// The authentication "Scheme" - /// - public string AuthenticationType { get; } - - public MemberExternalLoginProviderOptions Options { get; } - - public override bool Equals(object? obj) => Equals(obj as MemberExternalLoginProvider); - public bool Equals(MemberExternalLoginProvider? other) => other != null && AuthenticationType == other.AuthenticationType; - public override int GetHashCode() => HashCode.Combine(AuthenticationType); + AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType)); + Options = properties.Get(authenticationType); } + /// + /// The authentication "Scheme" + /// + public string AuthenticationType { get; } + + public MemberExternalLoginProviderOptions Options { get; } + + public bool Equals(MemberExternalLoginProvider? other) => + other != null && AuthenticationType == other.AuthenticationType; + + public override bool Equals(object? obj) => Equals(obj as MemberExternalLoginProvider); + + public override int GetHashCode() => HashCode.Combine(AuthenticationType); } diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs index 3a98b701d7..df9a8f6e2d 100644 --- a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs @@ -1,26 +1,22 @@ -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// Options used to configure member external login providers +/// +public class MemberExternalLoginProviderOptions { - /// - /// Options used to configure member external login providers - /// - public class MemberExternalLoginProviderOptions + public MemberExternalLoginProviderOptions( + MemberExternalSignInAutoLinkOptions? autoLinkOptions = null, + bool autoRedirectLoginToExternalProvider = false, + string? customBackOfficeView = null) => + AutoLinkOptions = autoLinkOptions ?? new MemberExternalSignInAutoLinkOptions(); + + public MemberExternalLoginProviderOptions() { - public MemberExternalLoginProviderOptions( - MemberExternalSignInAutoLinkOptions? autoLinkOptions = null, - bool autoRedirectLoginToExternalProvider = false, - string? customBackOfficeView = null) - { - AutoLinkOptions = autoLinkOptions ?? new MemberExternalSignInAutoLinkOptions(); - } - - public MemberExternalLoginProviderOptions() - { - } - - /// - /// Options used to control how users can be auto-linked/created/updated based on the external login provider - /// - public MemberExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new MemberExternalSignInAutoLinkOptions(); - } + + /// + /// Options used to control how users can be auto-linked/created/updated based on the external login provider + /// + public MemberExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new(); } diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs index 423fa899e4..103a592a2e 100644 --- a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs @@ -1,20 +1,18 @@ -using System; using Microsoft.AspNetCore.Authentication; -namespace Umbraco.Cms.Web.Common.Security -{ - public class MemberExternalLoginProviderScheme - { - public MemberExternalLoginProviderScheme( - MemberExternalLoginProvider externalLoginProvider, - AuthenticationScheme? authenticationScheme) - { - ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider)); - AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme)); - } +namespace Umbraco.Cms.Web.Common.Security; - public MemberExternalLoginProvider ExternalLoginProvider { get; } - public AuthenticationScheme AuthenticationScheme { get; } +public class MemberExternalLoginProviderScheme +{ + public MemberExternalLoginProviderScheme( + MemberExternalLoginProvider externalLoginProvider, + AuthenticationScheme? authenticationScheme) + { + ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider)); + AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme)); } + public MemberExternalLoginProvider ExternalLoginProvider { get; } + + public AuthenticationScheme AuthenticationScheme { get; } } diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs index 41b298d903..50e4dcb068 100644 --- a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs @@ -1,64 +1,59 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +public class MemberExternalLoginProviders : IMemberExternalLoginProviders { + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + private readonly Dictionary _externalLogins; - /// - public class MemberExternalLoginProviders : IMemberExternalLoginProviders + public MemberExternalLoginProviders( + IEnumerable externalLogins, + IAuthenticationSchemeProvider authenticationSchemeProvider) { - private readonly Dictionary _externalLogins; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - - public MemberExternalLoginProviders( - IEnumerable externalLogins, - IAuthenticationSchemeProvider authenticationSchemeProvider) - { - _externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType); - _authenticationSchemeProvider = authenticationSchemeProvider; - } - - /// - public async Task GetAsync(string authenticationType) - { - var schemaName = - authenticationType.EnsureStartsWith(Core.Constants.Security.MemberExternalAuthenticationTypePrefix); - - if (!_externalLogins.TryGetValue(schemaName, out MemberExternalLoginProvider? provider)) - { - return null; - } - - // get the associated scheme - AuthenticationScheme? associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType); - - if (associatedScheme == null) - { - throw new InvalidOperationException("No authentication scheme registered for " + provider.AuthenticationType); - } - - return new MemberExternalLoginProviderScheme(provider, associatedScheme); - } - - /// - public async Task> GetMemberProvidersAsync() - { - var providersWithSchemes = new List(); - foreach (MemberExternalLoginProvider login in _externalLogins.Values) - { - // get the associated scheme - AuthenticationScheme? associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType); - - providersWithSchemes.Add(new MemberExternalLoginProviderScheme(login, associatedScheme)); - } - - return providersWithSchemes; - } - + _externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType); + _authenticationSchemeProvider = authenticationSchemeProvider; } + /// + public async Task GetAsync(string authenticationType) + { + var schemaName = + authenticationType.EnsureStartsWith(Core.Constants.Security.MemberExternalAuthenticationTypePrefix); + + if (!_externalLogins.TryGetValue(schemaName, out MemberExternalLoginProvider? provider)) + { + return null; + } + + // get the associated scheme + AuthenticationScheme? associatedScheme = + await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType); + + if (associatedScheme == null) + { + throw new InvalidOperationException( + "No authentication scheme registered for " + provider.AuthenticationType); + } + + return new MemberExternalLoginProviderScheme(provider, associatedScheme); + } + + /// + public async Task> GetMemberProvidersAsync() + { + var providersWithSchemes = new List(); + foreach (MemberExternalLoginProvider login in _externalLogins.Values) + { + // get the associated scheme + AuthenticationScheme? associatedScheme = + await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType); + + providersWithSchemes.Add(new MemberExternalLoginProviderScheme(login, associatedScheme)); + } + + return providersWithSchemes; + } } diff --git a/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs index 21ce550196..5c79399cb0 100644 --- a/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs @@ -1,76 +1,73 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; using SecurityConstants = Umbraco.Cms.Core.Constants.Security; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// Options used to configure auto-linking external OAuth providers +/// +public class MemberExternalSignInAutoLinkOptions { + private readonly string? _defaultCulture; + /// - /// Options used to configure auto-linking external OAuth providers + /// Initializes a new instance of the class. /// - public class MemberExternalSignInAutoLinkOptions + public MemberExternalSignInAutoLinkOptions( + bool autoLinkExternalAccount = false, + bool defaultIsApproved = true, + string defaultMemberTypeAlias = Core.Constants.Conventions.MemberTypes.DefaultAlias, + string? defaultCulture = null, + IEnumerable? defaultMemberGroups = null) { - private readonly string? _defaultCulture; - - /// - /// Initializes a new instance of the class. - /// - public MemberExternalSignInAutoLinkOptions( - bool autoLinkExternalAccount = false, - bool defaultIsApproved = true, - string defaultMemberTypeAlias = Core.Constants.Conventions.MemberTypes.DefaultAlias, - string? defaultCulture = null, - IEnumerable? defaultMemberGroups = null) - { - AutoLinkExternalAccount = autoLinkExternalAccount; - DefaultIsApproved = defaultIsApproved; - DefaultMemberTypeAlias = defaultMemberTypeAlias; - _defaultCulture = defaultCulture; - DefaultMemberGroups = defaultMemberGroups ?? Array.Empty(); - } - - /// - /// A callback executed during account auto-linking and before the user is persisted - /// - [IgnoreDataMember] - public Action? OnAutoLinking { get; set; } - - /// - /// A callback executed during every time a user authenticates using an external login. - /// returns a boolean indicating if sign in should continue or not. - /// - [IgnoreDataMember] - public Func? OnExternalLogin { get; set; } - - /// - /// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a - /// local user - /// - public bool AutoLinkExternalAccount { get; } - - /// - /// Gets the member type alias that auto linked members are created as - /// - public string DefaultMemberTypeAlias { get; } - - /// - /// Gets the IsApproved value for auto linked members. - /// - public bool DefaultIsApproved { get; } - - /// - /// Gets the default member groups to add the user in. - /// - public IEnumerable DefaultMemberGroups { get; } - - /// - /// The default Culture to use for auto-linking users - /// - // TODO: Should we use IDefaultCultureAccessor here instead? - public string GetUserAutoLinkCulture(GlobalSettings globalSettings) => - _defaultCulture ?? globalSettings.DefaultUILanguage; + AutoLinkExternalAccount = autoLinkExternalAccount; + DefaultIsApproved = defaultIsApproved; + DefaultMemberTypeAlias = defaultMemberTypeAlias; + _defaultCulture = defaultCulture; + DefaultMemberGroups = defaultMemberGroups ?? Array.Empty(); } + + /// + /// A callback executed during account auto-linking and before the user is persisted + /// + [IgnoreDataMember] + public Action? OnAutoLinking { get; set; } + + /// + /// A callback executed during every time a user authenticates using an external login. + /// returns a boolean indicating if sign in should continue or not. + /// + [IgnoreDataMember] + public Func? OnExternalLogin { get; set; } + + /// + /// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a + /// local user + /// + public bool AutoLinkExternalAccount { get; } + + /// + /// Gets the member type alias that auto linked members are created as + /// + public string DefaultMemberTypeAlias { get; } + + /// + /// Gets the IsApproved value for auto linked members. + /// + public bool DefaultIsApproved { get; } + + /// + /// Gets the default member groups to add the user in. + /// + public IEnumerable DefaultMemberGroups { get; } + + /// + /// The default Culture to use for auto-linking users + /// + // TODO: Should we use IDefaultCultureAccessor here instead? + public string GetUserAutoLinkCulture(GlobalSettings globalSettings) => + _defaultCulture ?? globalSettings.DefaultUILanguage; } diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 3c734ca8a7..19be3de489 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -14,224 +10,243 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class MemberManager : UmbracoUserManager, IMemberManager { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPublicAccessService _publicAccessService; + private readonly IMemberUserStore _store; + private MemberIdentityUser? _currentMember; - public class MemberManager : UmbracoUserManager, IMemberManager + public MemberManager( + IIpResolver ipResolver, + IMemberUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger, + IOptionsSnapshot passwordConfiguration, + IPublicAccessService publicAccessService, + IHttpContextAccessor httpContextAccessor) + : base( + ipResolver, + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + errors, + services, + logger, + passwordConfiguration) { - private readonly IMemberUserStore _store; - private readonly IPublicAccessService _publicAccessService; - private readonly IHttpContextAccessor _httpContextAccessor; - private MemberIdentityUser? _currentMember; + _store = store; + _publicAccessService = publicAccessService; + _httpContextAccessor = httpContextAccessor; + } - public MemberManager( - IIpResolver ipResolver, - IMemberUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger> logger, - IOptionsSnapshot passwordConfiguration, - IPublicAccessService publicAccessService, - IHttpContextAccessor httpContextAccessor) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, - services, logger, passwordConfiguration) + /// + public async Task IsMemberAuthorizedAsync( + IEnumerable? allowTypes = null, + IEnumerable? allowGroups = null, + IEnumerable? allowMembers = null) + { + if (allowTypes == null) { - _store = store; - _publicAccessService = publicAccessService; - _httpContextAccessor = httpContextAccessor; + allowTypes = Enumerable.Empty(); } - /// - public async Task IsMemberAuthorizedAsync(IEnumerable? allowTypes = null, IEnumerable? allowGroups = null, IEnumerable? allowMembers = null) + if (allowGroups == null) { - if (allowTypes == null) - { - allowTypes = Enumerable.Empty(); - } - - if (allowGroups == null) - { - allowGroups = Enumerable.Empty(); - } - - if (allowMembers == null) - { - allowMembers = Enumerable.Empty(); - } - - // Allow by default - var allowAction = true; - - if (IsLoggedIn() == false) - { - // If not logged on, not allowed - allowAction = false; - } - else - { - MemberIdentityUser? currentMember = await GetCurrentMemberAsync(); - - // If a member could not be resolved from the provider, we are clearly not authorized and can break right here - if (currentMember == null) - { - return false; - } - - int memberId = int.Parse(currentMember.Id, CultureInfo.InvariantCulture); - - // If types defined, check member is of one of those types - IList allowTypesList = allowTypes as IList ?? allowTypes.ToList(); - if (allowTypesList.Any(allowType => allowType != string.Empty)) - { - // Allow only if member's type is in list - allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(currentMember.MemberTypeAlias?.ToLowerInvariant()); - } - - // If specific members defined, check member is of one of those - var allowMembersList = allowMembers.ToList(); - if (allowAction && allowMembersList.Any()) - { - // Allow only if member's Id is in the list - allowAction = allowMembersList.Contains(memberId); - } - - // If groups defined, check member is of one of those groups - IList allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); - if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) - { - // Allow only if member is assigned to a group in the list - IList groups = await GetRolesAsync(currentMember); - allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); - } - } - - return allowAction; + allowGroups = Enumerable.Empty(); } - /// - public bool IsLoggedIn() + if (allowMembers == null) { - HttpContext? httpContext = _httpContextAccessor.HttpContext; - return httpContext?.User.Identity?.IsAuthenticated ?? false; + allowMembers = Enumerable.Empty(); } - /// - public async Task MemberHasAccessAsync(string path) + // Allow by default + var allowAction = true; + + if (IsLoggedIn() == false) { - if (await IsProtectedAsync(path)) - { - return await HasAccessAsync(path); - } - return true; + // If not logged on, not allowed + allowAction = false; } - - /// - public async Task> MemberHasAccessAsync(IEnumerable paths) - { - IReadOnlyDictionary protectedPaths = await IsProtectedAsync(paths); - - IEnumerable pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); - IReadOnlyDictionary pathsWithAccess = await HasAccessAsync(pathsWithProtection); - - var result = new Dictionary(); - foreach (var path in paths) - { - pathsWithAccess.TryGetValue(path, out var hasAccess); - // if it's not found it's false anyways - result[path] = !pathsWithProtection.Contains(path) || hasAccess; - } - return result; - } - - /// - /// - /// this is a cached call - /// - public Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success); - - /// - public Task> IsProtectedAsync(IEnumerable paths) - { - var result = new Dictionary(); - foreach (var path in paths) - { - //this is a cached call - result[path] = _publicAccessService.IsProtected(path).Success; - } - return Task.FromResult((IReadOnlyDictionary)result); - } - - /// - public async Task GetCurrentMemberAsync() - { - if (_currentMember == null) - { - if (!IsLoggedIn()) - { - return null; - } - _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User); - } - return _currentMember; - } - - /// - /// This will check if the member has access to this path - /// - /// - /// - /// - private async Task HasAccessAsync(string path) + else { MemberIdentityUser? currentMember = await GetCurrentMemberAsync(); - if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + + // If a member could not be resolved from the provider, we are clearly not authorized and can break right here + if (currentMember == null) { return false; } - return await _publicAccessService.HasAccessAsync( - path, - currentMember.UserName, - async () => await GetRolesAsync(currentMember)); + var memberId = int.Parse(currentMember.Id, CultureInfo.InvariantCulture); + + // If types defined, check member is of one of those types + IList allowTypesList = allowTypes as IList ?? allowTypes.ToList(); + if (allowTypesList.Any(allowType => allowType != string.Empty)) + { + // Allow only if member's type is in list + allowAction = allowTypesList.Select(x => x.ToLowerInvariant()) + .Contains(currentMember.MemberTypeAlias?.ToLowerInvariant()); + } + + // If specific members defined, check member is of one of those + var allowMembersList = allowMembers.ToList(); + if (allowAction && allowMembersList.Any()) + { + // Allow only if member's Id is in the list + allowAction = allowMembersList.Contains(memberId); + } + + // If groups defined, check member is of one of those groups + IList allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); + if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) + { + // Allow only if member is assigned to a group in the list + IList groups = await GetRolesAsync(currentMember); + allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()) + .Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); + } } - private async Task> HasAccessAsync(IEnumerable paths) + return allowAction; + } + + /// + public bool IsLoggedIn() + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + return httpContext?.User.Identity?.IsAuthenticated ?? false; + } + + /// + public async Task MemberHasAccessAsync(string path) + { + if (await IsProtectedAsync(path)) { - var result = new Dictionary(); - MemberIdentityUser? currentMember = await GetCurrentMemberAsync(); + return await HasAccessAsync(path); + } - if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + return true; + } + + /// + public async Task> MemberHasAccessAsync(IEnumerable paths) + { + IReadOnlyDictionary protectedPaths = await IsProtectedAsync(paths); + + IEnumerable pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); + IReadOnlyDictionary pathsWithAccess = await HasAccessAsync(pathsWithProtection); + + var result = new Dictionary(); + foreach (var path in paths) + { + pathsWithAccess.TryGetValue(path, out var hasAccess); + + // if it's not found it's false anyways + result[path] = !pathsWithProtection.Contains(path) || hasAccess; + } + + return result; + } + + /// + /// + /// this is a cached call + /// + public Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success); + + /// + public Task> IsProtectedAsync(IEnumerable paths) + { + var result = new Dictionary(); + foreach (var path in paths) + { + // this is a cached call + result[path] = _publicAccessService.IsProtected(path).Success; + } + + return Task.FromResult((IReadOnlyDictionary)result); + } + + /// + public async Task GetCurrentMemberAsync() + { + if (_currentMember == null) + { + if (!IsLoggedIn()) { - return result; + return null; } - // ensure we only lookup user roles once - IList? userRoles = null; - async Task> getUserRolesAsync() - { - if (userRoles != null) - { - return userRoles; - } + _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User); + } - userRoles = await GetRolesAsync(currentMember); - return userRoles; - } + return _currentMember; + } - foreach (var path in paths) - { - result[path] = await _publicAccessService.HasAccessAsync( - path, - currentMember.UserName, - async () => await getUserRolesAsync()); - } + public IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user); + + /// + /// This will check if the member has access to this path + /// + /// + /// + private async Task HasAccessAsync(string path) + { + MemberIdentityUser? currentMember = await GetCurrentMemberAsync(); + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + { + return false; + } + + return await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await GetRolesAsync(currentMember)); + } + + private async Task> HasAccessAsync(IEnumerable paths) + { + var result = new Dictionary(); + MemberIdentityUser? currentMember = await GetCurrentMemberAsync(); + + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + { return result; } - public IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user); + // ensure we only lookup user roles once + IList? userRoles = null; + + async Task> GetUserRolesAsync() + { + if (userRoles != null) + { + return userRoles; + } + + userRoles = await GetRolesAsync(currentMember); + return userRoles; + } + + foreach (var path in paths) + { + result[path] = await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await GetUserRolesAsync()); + } + + return result; } } diff --git a/src/Umbraco.Web.Common/Security/MemberRoleManager.cs b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs index c0cd18aeda..0a7b9d6a9a 100644 --- a/src/Umbraco.Web.Common/Security/MemberRoleManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.Security -{ - public class MemberRoleManager : RoleManager, IMemberRoleManager - { - public MemberRoleManager( - IRoleStore store, - IEnumerable> roleValidators, - IdentityErrorDescriber errors, - ILogger logger) - : base(store, roleValidators, new NoopLookupNormalizer(), errors, logger) { } +namespace Umbraco.Cms.Web.Common.Security; - IEnumerable IMemberRoleManager.Roles => base.Roles.ToList(); +public class MemberRoleManager : RoleManager, IMemberRoleManager +{ + public MemberRoleManager( + IRoleStore store, + IEnumerable> roleValidators, + IdentityErrorDescriber errors, + ILogger logger) + : base(store, roleValidators, new NoopLookupNormalizer(), errors, logger) + { } + + IEnumerable IMemberRoleManager.Roles => Roles.ToList(); } diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index e0f5f19457..a624129bab 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -16,366 +11,359 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// The sign in manager for members +/// +public class MemberSignInManager : UmbracoSignInManager, IMemberSignInManager { + private readonly IEventAggregator _eventAggregator; + private readonly IMemberExternalLoginProviders _memberExternalLoginProviders; + + public MemberSignInManager( + UserManager memberManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation, + IMemberExternalLoginProviders memberExternalLoginProviders, + IEventAggregator eventAggregator) + : base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + _memberExternalLoginProviders = memberExternalLoginProviders; + _eventAggregator = eventAggregator; + } + + [Obsolete("Use ctor with all params")] + public MemberSignInManager( + UserManager memberManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : this( + memberManager, + contextAccessor, + claimsFactory, + optionsAccessor, + logger, + schemes, + confirmation, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + // use default scheme for members + protected override string AuthenticationType => IdentityConstants.ApplicationScheme; + + // use default scheme for members + protected override string ExternalAuthenticationType => IdentityConstants.ExternalScheme; + + // use default scheme for members + protected override string TwoFactorAuthenticationType => IdentityConstants.TwoFactorUserIdScheme; + + // use default scheme for members + protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; + + public override async Task GetExternalLoginInfoAsync(string? expectedXsrf = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + AuthenticateResult auth = await Context.AuthenticateAsync(ExternalAuthenticationType); + IDictionary? items = auth.Properties?.Items; + if (auth.Principal == null || items == null) + { + Logger.LogDebug( + auth.Failure ?? + new NullReferenceException("Context.AuthenticateAsync(ExternalAuthenticationType) is null"), + "The external login authentication failed. No user Principal or authentication items was resolved."); + return null; + } + + if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) + { + throw new InvalidOperationException( + $"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult."); + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) + { + return null; + } + + var userId = items[UmbracoSignInMgrXsrfKey]; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = items[UmbracoSignInMgrLoginProviderKey]; + if (providerKey == null || provider is null) + { + return null; + } + + var providerDisplayName = + (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? + provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties?.GetTokens(), + AuthenticationProperties = auth.Properties, + }; + } /// - /// The sign in manager for members + /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking /// - public class MemberSignInManager : UmbracoSignInManager, IMemberSignInManager + public async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false) { - private readonly IMemberExternalLoginProviders _memberExternalLoginProviders; - private readonly IEventAggregator _eventAggregator; - - public MemberSignInManager( - UserManager memberManager, - IHttpContextAccessor contextAccessor, - IUserClaimsPrincipalFactory claimsFactory, - IOptions optionsAccessor, - ILogger> logger, - IAuthenticationSchemeProvider schemes, - IUserConfirmation confirmation, - IMemberExternalLoginProviders memberExternalLoginProviders, - IEventAggregator eventAggregator) : - base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to be able to deal with auto-linking and reduce duplicate lookups + MemberExternalSignInAutoLinkOptions? autoLinkOptions = + (await _memberExternalLoginProviders.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider.Options.AutoLinkOptions; + MemberIdentityUser? user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (user == null) { - _memberExternalLoginProviders = memberExternalLoginProviders; - _eventAggregator = eventAggregator; + // user doesn't exist so see if we can auto link + return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions); } - [Obsolete("Use ctor with all params")] - public MemberSignInManager( - UserManager memberManager, - IHttpContextAccessor contextAccessor, - IUserClaimsPrincipalFactory claimsFactory, - IOptions optionsAccessor, - ILogger> logger, - IAuthenticationSchemeProvider schemes, - IUserConfirmation confirmation) : - this(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { } - - // use default scheme for members - protected override string AuthenticationType => IdentityConstants.ApplicationScheme; - - // use default scheme for members - protected override string ExternalAuthenticationType => IdentityConstants.ExternalScheme; - - // use default scheme for members - protected override string TwoFactorAuthenticationType => IdentityConstants.TwoFactorUserIdScheme; - - // use default scheme for members - protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; - - /// - public override async Task GetExternalLoginInfoAsync(string? expectedXsrf = null) + if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var auth = await Context.AuthenticateAsync(ExternalAuthenticationType); - var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null) + var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); + if (shouldSignIn == false) { - Logger.LogDebug(auth?.Failure ?? new NullReferenceException("Context.AuthenticateAsync(ExternalAuthenticationType) is null"), - "The external login authentication failed. No user Principal or authentication items was resolved."); - return null; - } - - if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) - { - throw new InvalidOperationException($"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult."); - } - - if (expectedXsrf != null) - { - if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) - { - return null; - } - var userId = items[UmbracoSignInMgrXsrfKey]; - if (userId != expectedXsrf) - { - return null; - } - } - - var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider) - { - return null; - } - - var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; - return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) - { - AuthenticationTokens = auth.Properties?.GetTokens(), - AuthenticationProperties = auth.Properties - }; - } - - /// - /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking - /// - public async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to be able to deal with auto-linking and reduce duplicate lookups - - var autoLinkOptions = (await _memberExternalLoginProviders.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider?.Options?.AutoLinkOptions; - var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); - if (user == null) - { - // user doesn't exist so see if we can auto link - return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions); - } - - if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) - { - var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); - if (shouldSignIn == false) - { - LogFailedExternalLogin(loginInfo, user); - return ExternalLoginSignInResult.NotAllowed; - } - } - - var error = await PreSignInCheck(user); - if (error != null) - { - return error; - } - return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor); - } - - - /// - /// Used for auto linking/creating user accounts for external logins - /// - /// - /// - /// - private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, MemberExternalSignInAutoLinkOptions? autoLinkOptions) - { - // If there are no autolink options then the attempt is failed (user does not exist) - if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount) - { - return SignInResult.Failed; - } - - var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email); - - //we are allowing auto-linking/creating of local accounts - if (email.IsNullOrWhiteSpace()) - { - return AutoLinkSignInResult.FailedNoEmail; - } - else - { - //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address - var autoLinkUser = await UserManager.FindByEmailAsync(email); - if (autoLinkUser != null) - { - try - { - //call the callback if one is assigned - autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); - return AutoLinkSignInResult.FailedException(ex.Message); - } - - var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); - if (shouldLinkUser) - { - return await LinkUser(autoLinkUser, loginInfo); - } - else - { - LogFailedExternalLogin(loginInfo, autoLinkUser); - return ExternalLoginSignInResult.NotAllowed; - } - } - else - { - var name = loginInfo.Principal?.Identity?.Name; - if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - - autoLinkUser = MemberIdentityUser.CreateNew(email, email, autoLinkOptions.DefaultMemberTypeAlias, autoLinkOptions.DefaultIsApproved, name); - - foreach (var userGroup in autoLinkOptions.DefaultMemberGroups) - { - autoLinkUser.AddRole(userGroup); - } - - //call the callback if one is assigned - try - { - autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); - return AutoLinkSignInResult.FailedException(ex.Message); - } - - var userCreationResult = await UserManager.CreateAsync(autoLinkUser); - - if (!userCreationResult.Succeeded) - { - return AutoLinkSignInResult.FailedCreatingUser(userCreationResult.Errors.Select(x => x.Description).ToList()); - } - else - { - var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); - if (shouldLinkUser) - { - return await LinkUser(autoLinkUser, loginInfo); - } - else - { - LogFailedExternalLogin(loginInfo, autoLinkUser); - return ExternalLoginSignInResult.NotAllowed; - } - } - } + LogFailedExternalLogin(loginInfo, user); + return ExternalLoginSignInResult.NotAllowed; } } - // TODO in v10 we can share this with backoffice by moving the backoffice into common. - public class ExternalLoginSignInResult : SignInResult + SignInResult? error = await PreSignInCheck(user); + if (error != null) { - public static ExternalLoginSignInResult NotAllowed { get; } = new ExternalLoginSignInResult() - { - Succeeded = false - }; - } - // TODO in v10 we can share this with backoffice by moving the backoffice into common. - public class AutoLinkSignInResult : SignInResult - { - public static AutoLinkSignInResult FailedNotLinked { get; } = new AutoLinkSignInResult() - { - Succeeded = false - }; - - public static AutoLinkSignInResult FailedNoEmail { get; } = new AutoLinkSignInResult() - { - Succeeded = false - }; - - public static AutoLinkSignInResult FailedException(string error) => new AutoLinkSignInResult(new[] { error }) - { - Succeeded = false - }; - - public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors) - { - Succeeded = false - }; - - public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors) - { - Succeeded = false - }; - - public AutoLinkSignInResult(IReadOnlyCollection errors) - { - Errors = errors ?? throw new ArgumentNullException(nameof(errors)); - } - - public AutoLinkSignInResult() - { - } - - public IReadOnlyCollection Errors { get; } = Array.Empty(); + return error; } - /// - public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string? userId = null) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to be able to use our own XsrfKey/LoginProviderKey because the default is private :/ + return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor); + } - var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; - properties.Items[UmbracoSignInMgrLoginProviderKey] = provider; - if (userId != null) - { - properties.Items[UmbracoSignInMgrXsrfKey] = userId; - } - return properties; + public override AuthenticationProperties ConfigureExternalAuthenticationProperties( + string provider, + string redirectUrl, + string? userId = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to be able to use our own XsrfKey/LoginProviderKey because the default is private :/ + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[UmbracoSignInMgrLoginProviderKey] = provider; + if (userId != null) + { + properties.Items[UmbracoSignInMgrXsrfKey] = userId; } - /// - public override Task> GetExternalAuthenticationSchemesAsync() + return properties; + } + + + protected override async Task SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent, string? loginProvider = null, bool bypassTwoFactor = false) + { + SignInResult result = await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); + + if (result.RequiresTwoFactor) { - // That can be done by either checking the scheme (maybe) or comparing it to what we have registered in the collection of BackOfficeExternalLoginProvider - return base.GetExternalAuthenticationSchemesAsync(); + NotifyRequiresTwoFactor(user); } - private async Task LinkUser(MemberIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) + return result; + } + + /// + /// Used for auto linking/creating user accounts for external logins + /// + /// + /// + /// + private async Task AutoLinkAndSignInExternalAccount( + ExternalLoginInfo loginInfo, + MemberExternalSignInAutoLinkOptions? autoLinkOptions) + { + // If there are no autolink options then the attempt is failed (user does not exist) + if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount) { - var existingLogins = await UserManager.GetLoginsAsync(autoLinkUser); - var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey); - - // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue - if (exists != null) - { - //sign in - return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider); - } - - var linkResult = await UserManager.AddLoginAsync(autoLinkUser, loginInfo); - if (linkResult.Succeeded) - { - //we're good! sign in - return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider); - } - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await UserManager.DeleteAsync(autoLinkUser); - if (deleteResult.Succeeded) - { - var errors = linkResult.Errors.Select(x => x.Description).ToList(); - return AutoLinkSignInResult.FailedLinkingUser(errors); - } - else - { - //DOH! ... this isn't good, combine all errors to be shown - var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList(); - return AutoLinkSignInResult.FailedLinkingUser(errors); - } + return SignInResult.Failed; } - private void LogFailedExternalLogin(ExternalLoginInfo loginInfo, MemberIdentityUser user) => - Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id); + var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email); - protected override async Task SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent, - string? loginProvider = null, bool bypassTwoFactor = false) + // we are allowing auto-linking/creating of local accounts + if (email.IsNullOrWhiteSpace()) { - var result = await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); - - if (result.RequiresTwoFactor) - { - NotifyRequiresTwoFactor(user); - } - - return result; + return AutoLinkSignInResult.FailedNoEmail; } - protected void NotifyRequiresTwoFactor(MemberIdentityUser user) => Notify(user, - (currentUser) => new MemberTwoFactorRequestedNotification(currentUser.Key) - ); - - private T Notify(MemberIdentityUser currentUser, Func createNotification) where T : INotification + // Now we need to perform the auto-link, so first we need to lookup/create a user with the email address + MemberIdentityUser? autoLinkUser = await UserManager.FindByEmailAsync(email); + if (autoLinkUser != null) { + try + { + // call the callback if one is assigned + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + return AutoLinkSignInResult.FailedException(ex.Message); + } - var notification = createNotification(currentUser); - _eventAggregator.Publish(notification); - return notification; + var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || + autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); + if (shouldLinkUser) + { + return await LinkUser(autoLinkUser, loginInfo); + } + + LogFailedExternalLogin(loginInfo, autoLinkUser); + return ExternalLoginSignInResult.NotAllowed; + } + + var name = loginInfo.Principal?.Identity?.Name; + if (name.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The Name value cannot be null"); + } + + autoLinkUser = MemberIdentityUser.CreateNew(email, email, autoLinkOptions.DefaultMemberTypeAlias, autoLinkOptions.DefaultIsApproved, name); + + foreach (var userGroup in autoLinkOptions.DefaultMemberGroups) + { + autoLinkUser.AddRole(userGroup); + } + + // call the callback if one is assigned + try + { + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + return AutoLinkSignInResult.FailedException(ex.Message); + } + + IdentityResult? userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (!userCreationResult.Succeeded) + { + return AutoLinkSignInResult.FailedCreatingUser( + userCreationResult.Errors.Select(x => x.Description).ToList()); + } + + { + var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || + autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); + if (shouldLinkUser) + { + return await LinkUser(autoLinkUser, loginInfo); + } + + LogFailedExternalLogin(loginInfo, autoLinkUser); + return ExternalLoginSignInResult.NotAllowed; } } + + private async Task LinkUser(MemberIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) + { + IList? existingLogins = await UserManager.GetLoginsAsync(autoLinkUser); + UserLoginInfo? exists = existingLogins.FirstOrDefault(x => + x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey); + + // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue + if (exists != null) + { + // sign in + return await SignInOrTwoFactorAsync(autoLinkUser, false, loginInfo.LoginProvider); + } + + IdentityResult? linkResult = await UserManager.AddLoginAsync(autoLinkUser, loginInfo); + if (linkResult.Succeeded) + { + // we're good! sign in + return await SignInOrTwoFactorAsync(autoLinkUser, false, loginInfo.LoginProvider); + } + + // If this fails, we should really delete the user since it will be in an inconsistent state! + IdentityResult? deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded) + { + var errors = linkResult.Errors.Select(x => x.Description).ToList(); + return AutoLinkSignInResult.FailedLinkingUser(errors); + } + else + { + // DOH! ... this isn't good, combine all errors to be shown + var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList(); + return AutoLinkSignInResult.FailedLinkingUser(errors); + } + } + + private void LogFailedExternalLogin(ExternalLoginInfo loginInfo, MemberIdentityUser user) => + Logger.LogWarning( + "The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", + loginInfo.LoginProvider, + user.Id); + + protected void NotifyRequiresTwoFactor(MemberIdentityUser user) => Notify( + user, + currentUser => new MemberTwoFactorRequestedNotification(currentUser.Key)); + + private T Notify(MemberIdentityUser currentUser, Func createNotification) + where T : INotification + { + T notification = createNotification(currentUser); + _eventAggregator.Publish(notification); + return notification; + } + + // TODO in v10 we can share this with backoffice by moving the backoffice into common. + public class ExternalLoginSignInResult : SignInResult + { + public static new ExternalLoginSignInResult NotAllowed { get; } = new() { Succeeded = false }; + } + + // TODO in v10 we can share this with backoffice by moving the backoffice into common. + public class AutoLinkSignInResult : SignInResult + { + public AutoLinkSignInResult(IReadOnlyCollection errors) => + Errors = errors ?? throw new ArgumentNullException(nameof(errors)); + + public AutoLinkSignInResult() + { + } + + public static AutoLinkSignInResult FailedNotLinked { get; } = new() { Succeeded = false }; + + public static AutoLinkSignInResult FailedNoEmail { get; } = new() { Succeeded = false }; + + public IReadOnlyCollection Errors { get; } = Array.Empty(); + + public static AutoLinkSignInResult FailedException(string error) => new(new[] { error }) { Succeeded = false }; + + public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection errors) => + new(errors) { Succeeded = false }; + + public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection errors) => + new(errors) { Succeeded = false }; + } } diff --git a/src/Umbraco.Web.Common/Security/NoopBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web.Common/Security/NoopBackOfficeUserPasswordChecker.cs index 4c8e52be84..04d9d8c2cc 100644 --- a/src/Umbraco.Web.Common/Security/NoopBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web.Common/Security/NoopBackOfficeUserPasswordChecker.cs @@ -1,11 +1,9 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class NoopBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker { - public class NoopBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker - { - public Task CheckPasswordAsync(BackOfficeIdentityUser user, string password) - => Task.FromResult(BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker); - } + public Task CheckPasswordAsync(BackOfficeIdentityUser user, string password) + => Task.FromResult(BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker); } diff --git a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs index ad9b39a7bb..b8b662ef2b 100644 --- a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs +++ b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs @@ -1,60 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +public class PublicAccessChecker : IPublicAccessChecker { - public class PublicAccessChecker : IPublicAccessChecker + private readonly IContentService _contentService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPublicAccessService _publicAccessService; + + public PublicAccessChecker(IHttpContextAccessor httpContextAccessor, IPublicAccessService publicAccessService, IContentService contentService) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IPublicAccessService _publicAccessService; - private readonly IContentService _contentService; + _httpContextAccessor = httpContextAccessor; + _publicAccessService = publicAccessService; + _contentService = contentService; + } - public PublicAccessChecker(IHttpContextAccessor httpContextAccessor, IPublicAccessService publicAccessService, IContentService contentService) + public async Task HasMemberAccessToContentAsync(int publishedContentId) + { + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + IMemberManager memberManager = httpContext.RequestServices.GetRequiredService(); + if (httpContext.User.Identity == null || !httpContext.User.Identity.IsAuthenticated) { - _httpContextAccessor = httpContextAccessor; - _publicAccessService = publicAccessService; - _contentService = contentService; + return PublicAccessStatus.NotLoggedIn; } - public async Task HasMemberAccessToContentAsync(int publishedContentId) + MemberIdentityUser? currentMember = await memberManager.GetUserAsync(httpContext.User); + if (currentMember == null) { - HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); - IMemberManager memberManager = httpContext.RequestServices.GetRequiredService(); - if (httpContext.User.Identity == null || !httpContext.User.Identity.IsAuthenticated) - { - return PublicAccessStatus.NotLoggedIn; - } - MemberIdentityUser currentMember = await memberManager.GetUserAsync(httpContext.User); - if (currentMember == null) - { - return PublicAccessStatus.NotLoggedIn; - } - - var username = currentMember.UserName; - IList userRoles = await memberManager.GetRolesAsync(currentMember); - - if (!currentMember.IsApproved) - { - return PublicAccessStatus.NotApproved; - } - - if (currentMember.IsLockedOut) - { - return PublicAccessStatus.LockedOut; - } - - if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) - { - return PublicAccessStatus.AccessDenied; - } - - return PublicAccessStatus.AccessAccepted; + return PublicAccessStatus.NotLoggedIn; } + + var username = currentMember.UserName; + IList userRoles = await memberManager.GetRolesAsync(currentMember); + + if (!currentMember.IsApproved) + { + return PublicAccessStatus.NotApproved; + } + + if (currentMember.IsLockedOut) + { + return PublicAccessStatus.LockedOut; + } + + if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) + { + return PublicAccessStatus.AccessDenied; + } + + return PublicAccessStatus.AccessAccepted; } } diff --git a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs index d4272515e5..cb9c539253 100644 --- a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs +++ b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs @@ -1,91 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Microsoft.Extensions.Logging; +namespace Umbraco.Cms.Infrastructure.Security; -namespace Umbraco.Cms.Infrastructure.Security +public class TwoFactorBackOfficeValidationProvider : TwoFactorValidationProvider + where TTwoFactorSetupGenerator : ITwoFactorProvider { - public class TwoFactorBackOfficeValidationProvider : TwoFactorValidationProvider - where TTwoFactorSetupGenerator : ITwoFactorProvider + public TwoFactorBackOfficeValidationProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger, + ITwoFactorLoginService twoFactorLoginService, + TTwoFactorSetupGenerator generator) + : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) { - public TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) - { - } - - } - - public class TwoFactorMemberValidationProvider : TwoFactorValidationProvider - where TTwoFactorSetupGenerator : ITwoFactorProvider - { - public TwoFactorMemberValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) - { - } - - } - - public class TwoFactorValidationProvider - : DataProtectorTokenProvider - where TUmbracoIdentityUser : UmbracoIdentityUser - where TTwoFactorSetupGenerator : ITwoFactorProvider - { - private readonly ITwoFactorLoginService _twoFactorLoginService; - private readonly TTwoFactorSetupGenerator _generator; - - protected TwoFactorValidationProvider( - - IDataProtectionProvider dataProtectionProvider, - IOptions options, - ILogger> logger, - ITwoFactorLoginService twoFactorLoginService, - TTwoFactorSetupGenerator generator) - : base(dataProtectionProvider, options, logger) - { - _twoFactorLoginService = twoFactorLoginService; - _generator = generator; - } - - public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, - TUmbracoIdentityUser user) => Task.FromResult(_generator is not null); - - public override async Task ValidateAsync(string purpose, string token, - UserManager manager, TUmbracoIdentityUser user) - { - var secret = - await _twoFactorLoginService.GetSecretForUserAndProviderAsync(GetUserKey(user), _generator.ProviderName); - - if (secret is null) - { - return false; - } - - var validToken = _generator.ValidateTwoFactorPIN(secret, token); - - - return validToken; - } - - protected Guid GetUserKey(TUmbracoIdentityUser user) - { - - switch (user) - { - case MemberIdentityUser memberIdentityUser: - return memberIdentityUser.Key; - case BackOfficeIdentityUser backOfficeIdentityUser: - return backOfficeIdentityUser.Key; - default: - throw new NotSupportedException( - "Current we only support MemberIdentityUser and BackOfficeIdentityUser"); - } - - } - + } +} + +public class TwoFactorMemberValidationProvider : TwoFactorValidationProvider + where TTwoFactorSetupGenerator : ITwoFactorProvider +{ + public TwoFactorMemberValidationProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger, + ITwoFactorLoginService twoFactorLoginService, + TTwoFactorSetupGenerator generator) + : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) + { + } +} + +public class TwoFactorValidationProvider + : DataProtectorTokenProvider + where TUmbracoIdentityUser : UmbracoIdentityUser + where TTwoFactorSetupGenerator : ITwoFactorProvider +{ + private readonly TTwoFactorSetupGenerator _generator; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + protected TwoFactorValidationProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger, + ITwoFactorLoginService twoFactorLoginService, + TTwoFactorSetupGenerator generator) + : base(dataProtectionProvider, options, logger) + { + _twoFactorLoginService = twoFactorLoginService; + _generator = generator; + } + + public override Task CanGenerateTwoFactorTokenAsync( + UserManager manager, + TUmbracoIdentityUser user) => Task.FromResult(_generator is not null); + + public override async Task ValidateAsync(string purpose, string token, UserManager manager, TUmbracoIdentityUser user) + { + var secret = + await _twoFactorLoginService.GetSecretForUserAndProviderAsync(GetUserKey(user), _generator.ProviderName); + + if (secret is null) + { + return false; + } + + var validToken = _generator.ValidateTwoFactorPIN(secret, token); + + return validToken; + } + + protected Guid GetUserKey(TUmbracoIdentityUser user) + { + switch (user) + { + case MemberIdentityUser memberIdentityUser: + return memberIdentityUser.Key; + case BackOfficeIdentityUser backOfficeIdentityUser: + return backOfficeIdentityUser.Key; + default: + throw new NotSupportedException( + "Current we only support MemberIdentityUser and BackOfficeIdentityUser"); + } } } diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index f47bb44c54..263343b817 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -11,460 +7,478 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security +namespace Umbraco.Cms.Web.Common.Security; + +/// +/// Abstract sign in manager implementation allowing modifying all defeault authentication schemes +/// +/// +public abstract class UmbracoSignInManager : SignInManager + where TUser : UmbracoIdentityUser { - /// - /// Abstract sign in manager implementation allowing modifying all defeault authentication schemes - /// - /// - public abstract class UmbracoSignInManager : SignInManager - where TUser : UmbracoIdentityUser + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrXsrfKey = "XsrfId"; + + public UmbracoSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - protected const string UmbracoSignInMgrXsrfKey = "XsrfId"; + } - public UmbracoSignInManager( - UserManager userManager, - IHttpContextAccessor contextAccessor, - IUserClaimsPrincipalFactory claimsFactory, - IOptions optionsAccessor, - ILogger> logger, - IAuthenticationSchemeProvider schemes, - IUserConfirmation confirmation) - : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + protected abstract string AuthenticationType { get; } + + protected abstract string ExternalAuthenticationType { get; } + + protected abstract string TwoFactorAuthenticationType { get; } + + protected abstract string TwoFactorRememberMeAuthenticationType { get; } + + /// + public override async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + return await HandleSignIn(user, user.UserName, result); + } + + /// + public override async Task GetExternalLoginInfoAsync(string? expectedXsrf = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + AuthenticateResult auth = await Context.AuthenticateAsync(ExternalAuthenticationType); + IDictionary? items = auth.Properties?.Items; + if (auth.Principal == null || items == null) { - } - - protected abstract string AuthenticationType { get; } - protected abstract string ExternalAuthenticationType { get; } - protected abstract string TwoFactorAuthenticationType { get; } - protected abstract string TwoFactorRememberMeAuthenticationType { get; } - - /// - public override async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - return await HandleSignIn(user, user.UserName, result); - } - - /// - public override async Task GetExternalLoginInfoAsync(string? expectedXsrf = null) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var auth = await Context.AuthenticateAsync(ExternalAuthenticationType); - var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null) - { - Logger.LogDebug("The external login authentication failed. No user Principal or authentication items was resolved."); - return null; - } - - if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) - { - throw new InvalidOperationException($"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult."); - } - - if (expectedXsrf != null) - { - if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) - { - return null; - } - var userId = items[UmbracoSignInMgrXsrfKey]; - if (userId != expectedXsrf) - { - return null; - } - } - - var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider) - { - return null; - } - - var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; - return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) - { - AuthenticationTokens = auth.Properties?.GetTokens(), - AuthenticationProperties = auth.Properties - }; - } - - /// - public override async Task GetTwoFactorAuthenticationUserAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // replaced in order to use a custom auth type - - var info = await RetrieveTwoFactorInfoAsync(); - if (info == null) - { - return null!; - } - return await UserManager.FindByIdAsync(info.UserId); - } - - /// - public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - var user = await UserManager.FindByNameAsync(userName); - if (user == null) - { - return await HandleSignIn(null, userName, SignInResult.Failed); - } - - return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - } - - /// - public override bool IsSignedIn(ClaimsPrincipal principal) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - return principal?.Identities != null && - principal.Identities.Any(i => i.AuthenticationType == AuthenticationType); - } - - /// - public override async Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 - // replaced in order to use a custom auth type and to implement logging/events - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var error = await PreSignInCheck(user); - if (error != null) - { - return error; - } - if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); - return await HandleSignIn(user, user?.UserName, SignInResult.Success); - } - // If the token is incorrect, record the failure which also may cause the user to be locked out - await UserManager.AccessFailedAsync(user); - return await HandleSignIn(user, user?.UserName, SignInResult.Failed); - } - - /// - public override async Task RefreshSignInAsync(TUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - var auth = await Context.AuthenticateAsync(AuthenticationType); - IList claims = Array.Empty(); - - var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); - var amr = auth?.Principal?.FindFirst("amr"); - - if (authenticationMethod != null || amr != null) - { - claims = new List(); - if (authenticationMethod != null) - { - claims.Add(authenticationMethod); - } - if (amr != null) - { - claims.Add(amr); - } - } - - await SignInWithClaimsAsync(user, auth?.Properties, claims); - } - - /// - public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties? authenticationProperties, IEnumerable additionalClaims) - { - // override to replace IdentityConstants.ApplicationScheme with custom AuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // we also override to set the current HttpContext principal since this isn't done by default - - // we also need to call our handle login to ensure all date/events are set - await HandleSignIn(user, user.UserName, SignInResult.Success); - - var userPrincipal = await CreateUserPrincipalAsync(user); - foreach (var claim in additionalClaims) - { - userPrincipal.Identities.First().AddClaim(claim); - } - - // FYI (just for informational purposes): - // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` - // which then resolves the `IAuthenticationSignInHandler` for the current scheme - // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` - // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync - - // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our - // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created - - // Also note, this method gets called when performing 2FA logins - - await Context.SignInAsync( - AuthenticationType, - userPrincipal, - authenticationProperties ?? new AuthenticationProperties()); - } - - /// - public override async Task SignOutAsync() - { - // override to replace IdentityConstants.ApplicationScheme with custom auth types - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - - await Context.SignOutAsync(AuthenticationType); - await Context.SignOutAsync(ExternalAuthenticationType); - await Context.SignOutAsync(TwoFactorAuthenticationType); - } - - /// - public override async Task IsTwoFactorClientRememberedAsync(TUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var userId = await UserManager.GetUserIdAsync(user); - var result = await Context.AuthenticateAsync(TwoFactorRememberMeAuthenticationType); - return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId); - } - - /// - public override async Task RememberTwoFactorClientAsync(TUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var principal = await StoreRememberClient(user); - await Context.SignInAsync(TwoFactorRememberMeAuthenticationType, - principal, - new AuthenticationProperties { IsPersistent = true }); - } - - /// - public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); - if (result.Succeeded) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); - return SignInResult.Success; - } - - // We don't protect against brute force attacks since codes are expected to be random. - return SignInResult.Failed; - } - - /// - public override Task ForgetTwoFactorClientAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - return Context.SignOutAsync(TwoFactorRememberMeAuthenticationType); - } - - /// - /// Called on any login attempt to update the AccessFailedCount and to raise events - /// - /// - /// - /// - /// - protected virtual async Task HandleSignIn(TUser? user, string? username, SignInResult result) - { - // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController - - if (username.IsNullOrWhiteSpace()) - { - username = "UNKNOWN"; // could happen in 2fa or something else weird - } - - if (result.Succeeded) - { - //track the last login date - user!.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - { - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - } - await UserManager.UpdateAsync(user); - - Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); - } - else if (result.IsLockedOut) - { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); - } - else if (result.RequiresTwoFactor) - { - Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); - } - else if (!result.Succeeded || result.IsNotAllowed) - { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); - } - else - { - throw new ArgumentOutOfRangeException(); - } - - return result; - } - - /// - protected override async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, string? loginProvider = null, bool bypassTwoFactor = false) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to replace custom auth types - - if (!bypassTwoFactor && await IsTfaEnabled(user)) - { - if (!await IsTwoFactorClientRememberedAsync(user)) - { - // Store the userId for use after two factor check - var userId = await UserManager.GetUserIdAsync(user); - await Context.SignInAsync(TwoFactorAuthenticationType, StoreTwoFactorInfo(userId, loginProvider)); - return SignInResult.TwoFactorRequired; - } - } - // Cleanup external cookie - if (loginProvider != null) - { - await Context.SignOutAsync(ExternalAuthenticationType); - } - if (loginProvider == null) - { - await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); - } - else - { - await SignInAsync(user, isPersistent, loginProvider); - } - return SignInResult.Success; - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 - // since it's not public - private async Task IsTfaEnabled(TUser user) - => UserManager.SupportsUserTwoFactor && - await UserManager.GetTwoFactorEnabledAsync(user) && - (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 - // to replace custom auth types - private ClaimsPrincipal StoreTwoFactorInfo(string userId, string? loginProvider) - { - var identity = new ClaimsIdentity(TwoFactorAuthenticationType); - identity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (loginProvider != null) - { - identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); - } - return new ClaimsPrincipal(identity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task StoreRememberClient(TUser user) - { - var userId = await UserManager.GetUserIdAsync(user); - var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberMeAuthenticationType); - rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (UserManager.SupportsUserSecurityStamp) - { - var stamp = await UserManager.GetSecurityStampAsync(user); - rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); - } - return new ClaimsPrincipal(rememberBrowserIdentity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use a custom auth type - private async Task RetrieveTwoFactorInfoAsync() - { - var result = await Context.AuthenticateAsync(TwoFactorAuthenticationType); - if (result?.Principal != null) - { - return new TwoFactorAuthenticationInfo - { - UserId = result.Principal.FindFirstValue(ClaimTypes.Name), - LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod) - }; - } + Logger.LogDebug( + "The external login authentication failed. No user Principal or authentication items was resolved."); return null; } + if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) + { + throw new InvalidOperationException( + $"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult."); + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) + { + return null; + } + + var userId = items[UmbracoSignInMgrXsrfKey]; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = items[UmbracoSignInMgrLoginProviderKey]; + if (providerKey == null || provider is not null) + { + return null; + } + + var providerDisplayName = + (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? + provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties?.GetTokens(), + AuthenticationProperties = auth.Properties, + }; + } + + /// + public override async Task GetTwoFactorAuthenticationUserAsync() + { // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) + // replaced in order to use a custom auth type + TwoFactorAuthenticationInfo? info = await RetrieveTwoFactorInfoAsync(); + if (info == null) { - // When token is verified correctly, clear the access failed count used for lockout - await ResetLockout(user); + return null!; + } - var claims = new List + return await UserManager.FindByIdAsync(info.UserId); + } + + /// + public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + TUser? user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return await HandleSignIn(null, userName, SignInResult.Failed); + } + + return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + } + + /// + public override bool IsSignedIn(ClaimsPrincipal principal) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.Identities.Any(i => i.AuthenticationType == AuthenticationType); + } + + /// + public override async Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 + // replaced in order to use a custom auth type and to implement logging/events + TwoFactorAuthenticationInfo? twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + + TUser? user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + SignInResult? error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + + if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); + return await HandleSignIn(user, user.UserName, SignInResult.Success); + } + + // If the token is incorrect, record the failure which also may cause the user to be locked out + await UserManager.AccessFailedAsync(user); + return await HandleSignIn(user, user.UserName, SignInResult.Failed); + } + + /// + public override async Task RefreshSignInAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + AuthenticateResult auth = await Context.AuthenticateAsync(AuthenticationType); + IList claims = Array.Empty(); + + Claim? authenticationMethod = auth.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); + Claim? amr = auth.Principal?.FindFirst("amr"); + + if (authenticationMethod != null || amr != null) + { + claims = new List(); + if (authenticationMethod != null) { - new Claim("amr", "mfa") + claims.Add(authenticationMethod); + } + + if (amr != null) + { + claims.Add(amr); + } + } + + await SignInWithClaimsAsync(user, auth.Properties, claims); + } + + /// + public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties? authenticationProperties, IEnumerable additionalClaims) + { + // override to replace IdentityConstants.ApplicationScheme with custom AuthenticationType + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // we also override to set the current HttpContext principal since this isn't done by default + + // we also need to call our handle login to ensure all date/events are set + await HandleSignIn(user, user.UserName, SignInResult.Success); + + ClaimsPrincipal? userPrincipal = await CreateUserPrincipalAsync(user); + foreach (Claim claim in additionalClaims) + { + userPrincipal.Identities.First().AddClaim(claim); + } + + // FYI (just for informational purposes): + // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` + // which then resolves the `IAuthenticationSignInHandler` for the current scheme + // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` + // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync + + // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our + // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created + + // Also note, this method gets called when performing 2FA logins + await Context.SignInAsync( + AuthenticationType, + userPrincipal, + authenticationProperties ?? new AuthenticationProperties()); + } + + /// + public override async Task SignOutAsync() + { + // override to replace IdentityConstants.ApplicationScheme with custom auth types + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + await Context.SignOutAsync(AuthenticationType); + await Context.SignOutAsync(ExternalAuthenticationType); + await Context.SignOutAsync(TwoFactorAuthenticationType); + } + + /// + public override async Task IsTwoFactorClientRememberedAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + var userId = await UserManager.GetUserIdAsync(user); + AuthenticateResult result = await Context.AuthenticateAsync(TwoFactorRememberMeAuthenticationType); + return result.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId; + } + + /// + public override async Task RememberTwoFactorClientAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + ClaimsPrincipal principal = await StoreRememberClient(user); + await Context.SignInAsync( + TwoFactorRememberMeAuthenticationType, + principal, + new AuthenticationProperties { IsPersistent = true }); + } + + /// + public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + TwoFactorAuthenticationInfo? twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + + TUser? user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + IdentityResult? result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); + if (result.Succeeded) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, false, false); + return SignInResult.Success; + } + + // We don't protect against brute force attacks since codes are expected to be random. + return SignInResult.Failed; + } + + /// + public override Task ForgetTwoFactorClientAsync() => + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + Context.SignOutAsync(TwoFactorRememberMeAuthenticationType); + + /// + /// Called on any login attempt to update the AccessFailedCount and to raise events + /// + /// + /// + /// + /// + protected virtual async Task HandleSignIn(TUser? user, string? username, SignInResult result) + { + // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController + if (username.IsNullOrWhiteSpace()) + { + username = "UNKNOWN"; // could happen in 2fa or something else weird + } + + if (result.Succeeded) + { + // track the last login date + user!.LastLoginDateUtc = DateTime.UtcNow; + if (user.AccessFailedCount > 0) + { + // we have successfully logged in, reset the AccessFailedCount + user.AccessFailedCount = 0; + } + + await UserManager.UpdateAsync(user); + + Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (result.IsLockedOut) + { + Logger.LogInformation( + "Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", + username, + Context.Connection.RemoteIpAddress); + } + else if (result.RequiresTwoFactor) + { + Logger.LogInformation( + "Login attempt requires verification for username {UserName} from IP address {IpAddress}", + username, + Context.Connection.RemoteIpAddress); + } + else if (!result.Succeeded || result.IsNotAllowed) + { + Logger.LogInformation( + "Login attempt failed for username {UserName} from IP address {IpAddress}", + username, + Context.Connection.RemoteIpAddress); + } + else + { + throw new ArgumentOutOfRangeException(); + } + + return result; + } + + /// + protected override async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, string? loginProvider = null, bool bypassTwoFactor = false) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to replace custom auth types + if (!bypassTwoFactor && await IsTfaEnabled(user)) + { + if (!await IsTwoFactorClientRememberedAsync(user)) + { + // Store the userId for use after two factor check + var userId = await UserManager.GetUserIdAsync(user); + await Context.SignInAsync(TwoFactorAuthenticationType, StoreTwoFactorInfo(userId, loginProvider)); + return SignInResult.TwoFactorRequired; + } + } + + // Cleanup external cookie + if (loginProvider != null) + { + await Context.SignOutAsync(ExternalAuthenticationType); + } + + if (loginProvider == null) + { + await SignInWithClaimsAsync(user, isPersistent, new[] { new Claim("amr", "pwd") }); + } + else + { + await SignInAsync(user, isPersistent, loginProvider); + } + + return SignInResult.Success; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 + // since it's not public + private async Task IsTfaEnabled(TUser user) + => UserManager.SupportsUserTwoFactor && + await UserManager.GetTwoFactorEnabledAsync(user) && + (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 + // to replace custom auth types + private ClaimsPrincipal StoreTwoFactorInfo(string userId, string? loginProvider) + { + var identity = new ClaimsIdentity(TwoFactorAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (loginProvider != null) + { + identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); + } + + return new ClaimsPrincipal(identity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task StoreRememberClient(TUser user) + { + var userId = await UserManager.GetUserIdAsync(user); + var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberMeAuthenticationType); + rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (UserManager.SupportsUserSecurityStamp) + { + var stamp = await UserManager.GetSecurityStampAsync(user); + rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); + } + + return new ClaimsPrincipal(rememberBrowserIdentity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use a custom auth type + private async Task RetrieveTwoFactorInfoAsync() + { + AuthenticateResult result = await Context.AuthenticateAsync(TwoFactorAuthenticationType); + if (result.Principal != null) + { + return new TwoFactorAuthenticationInfo + { + UserId = result.Principal.FindFirstValue(ClaimTypes.Name), + LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod), }; - - // Cleanup external cookie - if (twoFactorInfo.LoginProvider != null) - { - claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); - await Context.SignOutAsync(ExternalAuthenticationType); - } - // Cleanup two factor user id cookie - await Context.SignOutAsync(TwoFactorAuthenticationType); - if (rememberClient) - { - await RememberTwoFactorClientAsync(user); - } - await SignInWithClaimsAsync(user, isPersistent, claims); } - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 - private class TwoFactorAuthenticationInfo + return null; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) + { + // When token is verified correctly, clear the access failed count used for lockout + await ResetLockout(user); + + var claims = new List { new("amr", "mfa") }; + + // Cleanup external cookie + if (twoFactorInfo.LoginProvider != null) { - public string? UserId { get; set; } - public string? LoginProvider { get; set; } + claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); + await Context.SignOutAsync(ExternalAuthenticationType); } + + // Cleanup two factor user id cookie + await Context.SignOutAsync(TwoFactorAuthenticationType); + if (rememberClient) + { + await RememberTwoFactorClientAsync(user); + } + + await SignInWithClaimsAsync(user, isPersistent, claims); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 + private class TwoFactorAuthenticationInfo + { + public string? UserId { get; set; } + + public string? LoginProvider { get; set; } } } diff --git a/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs index 55a70e4f81..2badec9550 100644 --- a/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs @@ -1,8 +1,4 @@ -using System; using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -11,209 +7,217 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Templates +namespace Umbraco.Cms.Web.Common.Templates; + +/// +/// This is used purely for the RenderTemplate functionality in Umbraco +/// +/// +/// This allows you to render an MVC template based purely off of a node id and an optional alttemplate id as string +/// output. +/// +internal class TemplateRenderer : ITemplateRenderer { - /// - /// This is used purely for the RenderTemplate functionality in Umbraco - /// - /// - /// This allows you to render an MVC template based purely off of a node id and an optional alttemplate id as string output. - /// - internal class TemplateRenderer : ITemplateRenderer + private readonly IFileService _fileService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILocalizationService _languageService; + private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly IPublishedRouter _publishedRouter; + private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ICompositeViewEngine _viewEngine; + private WebRoutingSettings _webRoutingSettings; + + public TemplateRenderer( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + IFileService fileService, + ILocalizationService textService, + IOptionsMonitor webRoutingSettings, + IHttpContextAccessor httpContextAccessor, + ICompositeViewEngine viewEngine, + IModelMetadataProvider modelMetadataProvider, + ITempDataDictionaryFactory tempDataDictionaryFactory) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedRouter _publishedRouter; - private readonly IFileService _fileService; - private readonly ILocalizationService _languageService; - private WebRoutingSettings _webRoutingSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ICompositeViewEngine _viewEngine; - private readonly IModelMetadataProvider _modelMetadataProvider; - private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + _languageService = textService ?? throw new ArgumentNullException(nameof(textService)); + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine)); + _modelMetadataProvider = modelMetadataProvider; + _tempDataDictionaryFactory = tempDataDictionaryFactory; - public TemplateRenderer( - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedRouter publishedRouter, - IFileService fileService, - ILocalizationService textService, - IOptionsMonitor webRoutingSettings, - IHttpContextAccessor httpContextAccessor, - ICompositeViewEngine viewEngine, - IModelMetadataProvider modelMetadataProvider, - ITempDataDictionaryFactory tempDataDictionaryFactory) + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } + + public async Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer) + { + if (writer == null) { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); - _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); - _languageService = textService ?? throw new ArgumentNullException(nameof(textService)); - _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine)); - _modelMetadataProvider = modelMetadataProvider; - _tempDataDictionaryFactory = tempDataDictionaryFactory; - - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + throw new ArgumentNullException(nameof(writer)); } - public async Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer) + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // instantiate a request and process + // important to use CleanedUmbracoUrl - lowercase path-only version of the current URL, though this isn't going to matter + // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. + IPublishedRequestBuilder requestBuilder = + await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + + IPublishedContent? doc = umbracoContext.Content?.GetById(pageId); + + if (doc == null) { - if (writer == null) throw new ArgumentNullException(nameof(writer)); + writer.Write("", pageId); + return; + } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + // in some cases the UmbracoContext will not have a PublishedRequest assigned to it if we are not in the + // execution of a front-end rendered page. In this case set the culture to the default. + // set the culture to the same as is currently rendering + if (umbracoContext.PublishedRequest == null) + { + ILanguage? defaultLanguage = _languageService.GetAllLanguages().FirstOrDefault(); - // instantiate a request and process - // important to use CleanedUmbracoUrl - lowercase path-only version of the current URL, though this isn't going to matter - // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. - var requestBuilder = await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + requestBuilder.SetCulture(defaultLanguage == null + ? CultureInfo.CurrentUICulture.Name + : defaultLanguage.IsoCode); + } + else + { + requestBuilder.SetCulture(umbracoContext.PublishedRequest.Culture); + } - var doc = umbracoContext.Content?.GetById(pageId); + // set the doc that was found by id + requestBuilder.SetPublishedContent(doc); - if (doc == null) + // set the template, either based on the AltTemplate found or the standard template of the doc + var templateId = _webRoutingSettings.DisableAlternativeTemplates || !altTemplateId.HasValue + ? doc.TemplateId + : altTemplateId.Value; + + if (templateId.HasValue) + { + requestBuilder.SetTemplate(_fileService.GetTemplate(templateId.Value)); + } + + // if there is not template then exit + if (requestBuilder.HasTemplate() == false) + { + if (altTemplateId.HasValue == false) { - writer.Write("", pageId); - return; - } - - // in some cases the UmbracoContext will not have a PublishedRequest assigned to it if we are not in the - // execution of a front-end rendered page. In this case set the culture to the default. - // set the culture to the same as is currently rendering - if (umbracoContext.PublishedRequest == null) - { - var defaultLanguage = _languageService.GetAllLanguages().FirstOrDefault(); - - requestBuilder.SetCulture(defaultLanguage == null - ? CultureInfo.CurrentUICulture.Name - : defaultLanguage.IsoCode); + writer.Write( + "", + doc.TemplateId); } else { - requestBuilder.SetCulture(umbracoContext.PublishedRequest.Culture); - } - - // set the doc that was found by id - requestBuilder.SetPublishedContent(doc); - - // set the template, either based on the AltTemplate found or the standard template of the doc - var templateId = _webRoutingSettings.DisableAlternativeTemplates || !altTemplateId.HasValue - ? doc.TemplateId - : altTemplateId.Value; - - if (templateId.HasValue) - { - requestBuilder.SetTemplate(_fileService.GetTemplate(templateId.Value)); - } - - // if there is not template then exit - if (requestBuilder.HasTemplate() == false) - { - if (altTemplateId.HasValue == false) - { - writer.Write("", doc.TemplateId); - } - else - { - writer.Write("", altTemplateId); - } - - return; - } - - // First, save all of the items locally that we know are used in the chain of execution, we'll need to restore these - // after this page has rendered. - SaveExistingItems(out IPublishedRequest? oldPublishedRequest); - - IPublishedRequest contentRequest = requestBuilder.Build(); - - try - { - // set the new items on context objects for this templates execution - SetNewItemsOnContextObjects(contentRequest); - - // Render the template - ExecuteTemplateRendering(writer, contentRequest); - } - finally - { - // restore items on context objects to continuing rendering the parent template - RestoreItems(oldPublishedRequest); + writer.Write( + "", + altTemplateId); } + return; } - private void ExecuteTemplateRendering(TextWriter sw, IPublishedRequest request) + // First, save all of the items locally that we know are used in the chain of execution, we'll need to restore these + // after this page has rendered. + SaveExistingItems(out IPublishedRequest? oldPublishedRequest); + + IPublishedRequest contentRequest = requestBuilder.Build(); + + try { - var httpContext = _httpContextAccessor.GetRequiredHttpContext(); + // set the new items on context objects for this templates execution + SetNewItemsOnContextObjects(contentRequest); - // isMainPage is set to true here to ensure ViewStart(s) found in the view hierarchy are rendered - var viewResult = _viewEngine.GetView(null, $"~/Views/{request.GetTemplateAlias()}.cshtml", isMainPage: true); - - if (viewResult.Success == false) - { - throw new InvalidOperationException($"A view with the name {request.GetTemplateAlias()} could not be found"); - } - - var viewData = new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()) - { - Model = request.PublishedContent - }; - - var writer = new StringWriter(); - var viewContext = new ViewContext( - new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()), - viewResult.View, - viewData, - _tempDataDictionaryFactory.GetTempData(httpContext), - writer, - new HtmlHelperOptions() - ); - - - viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult(); - - var output = writer.GetStringBuilder().ToString(); - - sw.Write(output); + // Render the template + ExecuteTemplateRendering(writer, contentRequest); } - - // TODO: I feel like we need to do more than this, pretty sure we need to replace the UmbracoRouteValues - // HttpRequest feature too while this renders. - - private void SetNewItemsOnContextObjects(IPublishedRequest request) + finally { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - - // now, set the new ones for this page execution - umbracoContext.PublishedRequest = request; - } - - /// - /// Save all items that we know are used for rendering execution to variables so we can restore after rendering - /// - private void SaveExistingItems(out IPublishedRequest? oldPublishedRequest) - { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - // Many objects require that these legacy items are in the http context items... before we render this template we need to first - // save the values in them so that we can re-set them after we render so the rest of the execution works as per normal - oldPublishedRequest = umbracoContext.PublishedRequest; - } - - /// - /// Restores all items back to their context's to continue normal page rendering execution - /// - private void RestoreItems(IPublishedRequest? oldPublishedRequest) - { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - umbracoContext.PublishedRequest = oldPublishedRequest; + // restore items on context objects to continuing rendering the parent template + RestoreItems(oldPublishedRequest); } } + + private void ExecuteTemplateRendering(TextWriter sw, IPublishedRequest request) + { + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + + // isMainPage is set to true here to ensure ViewStart(s) found in the view hierarchy are rendered + ViewEngineResult viewResult = _viewEngine.GetView(null, $"~/Views/{request.GetTemplateAlias()}.cshtml", true); + + if (viewResult.Success == false) + { + throw new InvalidOperationException( + $"A view with the name {request.GetTemplateAlias()} could not be found"); + } + + var viewData = new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()) + { + Model = request.PublishedContent, + }; + + var writer = new StringWriter(); + var viewContext = new ViewContext( + new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()), + viewResult.View, + viewData, + _tempDataDictionaryFactory.GetTempData(httpContext), + writer, + new HtmlHelperOptions()); + + viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult(); + + var output = writer.GetStringBuilder().ToString(); + + sw.Write(output); + } + + // TODO: I feel like we need to do more than this, pretty sure we need to replace the UmbracoRouteValues + // HttpRequest feature too while this renders. + private void SetNewItemsOnContextObjects(IPublishedRequest request) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // now, set the new ones for this page execution + umbracoContext.PublishedRequest = request; + } + + /// + /// Save all items that we know are used for rendering execution to variables so we can restore after rendering + /// + private void SaveExistingItems(out IPublishedRequest? oldPublishedRequest) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // Many objects require that these legacy items are in the http context items... before we render this template we need to first + // save the values in them so that we can re-set them after we render so the rest of the execution works as per normal + oldPublishedRequest = umbracoContext.PublishedRequest; + } + + /// + /// Restores all items back to their context's to continue normal page rendering execution + /// + private void RestoreItems(IPublishedRequest? oldPublishedRequest) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + umbracoContext.PublishedRequest = oldPublishedRequest; + } } diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index c24db1502b..878824c383 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Umbraco.Cms.Core; @@ -8,167 +7,171 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.UmbracoContext +namespace Umbraco.Cms.Web.Common.UmbracoContext; + +/// +/// Class that encapsulates Umbraco information of a specific HTTP request +/// +public class UmbracoContext : DisposableObjectSlim, IUmbracoContext { - /// - /// Class that encapsulates Umbraco information of a specific HTTP request - /// - public class UmbracoContext : DisposableObjectSlim, IUmbracoContext + private readonly ICookieManager _cookieManager; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Lazy _publishedSnapshot; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly UriUtility _uriUtility; + private Uri? _cleanedUmbracoUrl; + private Uri? _originalRequestUrl; + private bool? _previewing; + private string? _previewToken; + private Uri? _requestUrl; + + // initializes a new instance of the UmbracoContext class + // internal for unit tests + // otherwise it's used by EnsureContext above + // warn: does *not* manage setting any IUmbracoContextAccessor + internal UmbracoContext( + IPublishedSnapshotService publishedSnapshotService, + UmbracoRequestPaths umbracoRequestPaths, + IHostingEnvironment hostingEnvironment, + UriUtility uriUtility, + ICookieManager cookieManager, + IHttpContextAccessor httpContextAccessor) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly UriUtility _uriUtility; - private readonly ICookieManager _cookieManager; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly Lazy _publishedSnapshot; - private string? _previewToken; - private bool? _previewing; - private readonly UmbracoRequestPaths _umbracoRequestPaths; - private Uri? _requestUrl; - private Uri? _originalRequestUrl; - private Uri? _cleanedUmbracoUrl; - - // initializes a new instance of the UmbracoContext class - // internal for unit tests - // otherwise it's used by EnsureContext above - // warn: does *not* manage setting any IUmbracoContextAccessor - internal UmbracoContext( - IPublishedSnapshotService publishedSnapshotService, - UmbracoRequestPaths umbracoRequestPaths, - IHostingEnvironment hostingEnvironment, - UriUtility uriUtility, - ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) + if (publishedSnapshotService == null) { - if (publishedSnapshotService == null) - { - throw new ArgumentNullException(nameof(publishedSnapshotService)); - } - _uriUtility = uriUtility; - _hostingEnvironment = hostingEnvironment; - _cookieManager = cookieManager; - _httpContextAccessor = httpContextAccessor; - ObjectCreated = DateTime.Now; - UmbracoRequestId = Guid.NewGuid(); - _umbracoRequestPaths = umbracoRequestPaths; - - // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing - _publishedSnapshot = new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); + throw new ArgumentNullException(nameof(publishedSnapshotService)); } - /// - public DateTime ObjectCreated { get; } + _uriUtility = uriUtility; + _hostingEnvironment = hostingEnvironment; + _cookieManager = cookieManager; + _httpContextAccessor = httpContextAccessor; + ObjectCreated = DateTime.Now; + UmbracoRequestId = Guid.NewGuid(); + _umbracoRequestPaths = umbracoRequestPaths; - /// - /// Gets the context Id - /// - /// - /// Used internally for debugging and also used to define anything required to distinguish this request from another. - /// - internal Guid UmbracoRequestId { get; } + // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing + _publishedSnapshot = + new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); + } - // lazily get/create a Uri for the current request - private Uri? RequestUrl => _requestUrl ??= _httpContextAccessor.HttpContext is null - ? null - : new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()); + /// + public DateTime ObjectCreated { get; } - /// - // set the urls lazily, no need to allocate until they are needed... - // NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this - // is a work around to being able to access the UmbracoContext during application startup and this will also ensure that people - // 'could' still generate URLs during startup BUT any domain driven URL generation will not work because it is NOT possible to get - // the current domain during application startup. - // see: http://issues.umbraco.org/issue/U4-1890 - public Uri OriginalRequestUrl => _originalRequestUrl ?? (_originalRequestUrl = RequestUrl ?? new Uri("http://localhost")); + /// + /// Gets the context Id + /// + /// + /// Used internally for debugging and also used to define anything required to distinguish this request from another. + /// + internal Guid UmbracoRequestId { get; } - /// - // set the urls lazily, no need to allocate until they are needed... - public Uri CleanedUmbracoUrl => _cleanedUmbracoUrl ?? (_cleanedUmbracoUrl = _uriUtility.UriToUmbraco(OriginalRequestUrl)); - - /// - public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; - - /// - public IPublishedContentCache? Content => PublishedSnapshot.Content; - - /// - public IPublishedMediaCache? Media => PublishedSnapshot.Media; - - /// - public IDomainCache? Domains => PublishedSnapshot.Domains; - - /// - public IPublishedRequest? PublishedRequest { get; set; } - - /// - public bool IsDebug => // NOTE: the request can be null during app startup! - _hostingEnvironment.IsDebugMode - && (string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) == false - || string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebug")) == false - || string.IsNullOrEmpty(_cookieManager.GetCookieValue("UMB-DEBUG")) == false); - - /// - public bool InPreviewMode + internal string? PreviewToken + { + get { - get + if (_previewing.HasValue == false) { - if (_previewing.HasValue == false) - { - DetectPreviewMode(); - } - - return _previewing ?? false; - } - private set => _previewing = value; - } - - internal string? PreviewToken - { - get - { - if (_previewing.HasValue == false) - { - DetectPreviewMode(); - } - - return _previewToken; - } - } - - private void DetectPreviewMode() - { - if (RequestUrl != null - && _umbracoRequestPaths.IsBackOfficeRequest(RequestUrl.AbsolutePath) == false - && _httpContextAccessor.HttpContext?.GetCurrentIdentity() != null) - { - var previewToken = _cookieManager.GetCookieValue(Core.Constants.Web.PreviewCookieName); // may be null or empty - _previewToken = previewToken.IsNullOrWhiteSpace() ? null : previewToken; + DetectPreviewMode(); } - _previewing = _previewToken.IsNullOrWhiteSpace() == false; - } - - /// - public IDisposable ForcedPreview(bool preview) - { - // say we render a macro or RTE in a give 'preview' mode that might not be the 'current' one, - // then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper - // default 'preview' mode - somehow we have to force it. and that could be recursive. - InPreviewMode = preview; - return PublishedSnapshot.ForcedPreview(preview, orig => InPreviewMode = orig); - } - - /// - protected override void DisposeResources() - { - // DisposableObject ensures that this runs only once - - // help caches release resources - // (but don't create caches just to dispose them) - // context is not multi-threaded - if (_publishedSnapshot.IsValueCreated) - { - _publishedSnapshot.Value.Dispose(); - } + return _previewToken; } } + + // lazily get/create a Uri for the current request + private Uri? RequestUrl => _requestUrl ??= _httpContextAccessor.HttpContext is null + ? null + : new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()); + + /// + // set the urls lazily, no need to allocate until they are needed... + // NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this + // is a work around to being able to access the UmbracoContext during application startup and this will also ensure that people + // 'could' still generate URLs during startup BUT any domain driven URL generation will not work because it is NOT possible to get + // the current domain during application startup. + // see: http://issues.umbraco.org/issue/U4-1890 + public Uri OriginalRequestUrl => +_originalRequestUrl ??= RequestUrl ?? new Uri("http://localhost"); + + /// + // set the urls lazily, no need to allocate until they are needed... + public Uri CleanedUmbracoUrl => +_cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); + + /// + public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; + + /// + public IPublishedContentCache? Content => PublishedSnapshot.Content; + + /// + public IPublishedMediaCache? Media => PublishedSnapshot.Media; + + /// + public IDomainCache? Domains => PublishedSnapshot.Domains; + + /// + public IPublishedRequest? PublishedRequest { get; set; } + + /// + public bool IsDebug => // NOTE: the request can be null during app startup! + _hostingEnvironment.IsDebugMode + && (string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) == false + || string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebug")) == false + || string.IsNullOrEmpty(_cookieManager.GetCookieValue("UMB-DEBUG")) == false); + + /// + public bool InPreviewMode + { + get + { + if (_previewing.HasValue == false) + { + DetectPreviewMode(); + } + + return _previewing ?? false; + } + private set => _previewing = value; + } + + /// + public IDisposable ForcedPreview(bool preview) + { + // say we render a macro or RTE in a give 'preview' mode that might not be the 'current' one, + // then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper + // default 'preview' mode - somehow we have to force it. and that could be recursive. + InPreviewMode = preview; + return PublishedSnapshot.ForcedPreview(preview, orig => InPreviewMode = orig); + } + + /// + protected override void DisposeResources() + { + // DisposableObject ensures that this runs only once + + // help caches release resources + // (but don't create caches just to dispose them) + // context is not multi-threaded + if (_publishedSnapshot.IsValueCreated) + { + _publishedSnapshot.Value.Dispose(); + } + } + + private void DetectPreviewMode() + { + if (RequestUrl != null + && _umbracoRequestPaths.IsBackOfficeRequest(RequestUrl.AbsolutePath) == false + && _httpContextAccessor.HttpContext?.GetCurrentIdentity() != null) + { + var previewToken = + _cookieManager.GetCookieValue(Core.Constants.Web.PreviewCookieName); // may be null or empty + _previewToken = previewToken.IsNullOrWhiteSpace() ? null : previewToken; + } + + _previewing = _previewToken.IsNullOrWhiteSpace() == false; + } } diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index 513b1e9b2e..726f96cf31 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; @@ -6,63 +5,63 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Web.Common.UmbracoContext +namespace Umbraco.Cms.Web.Common.UmbracoContext; + +/// +/// Creates and manages instances. +/// +public class UmbracoContextFactory : IUmbracoContextFactory { + private readonly ICookieManager _cookieManager; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly UriUtility _uriUtility; + /// - /// Creates and manages instances. + /// Initializes a new instance of the class. /// - public class UmbracoContextFactory : IUmbracoContextFactory + public UmbracoContextFactory( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedSnapshotService publishedSnapshotService, + UmbracoRequestPaths umbracoRequestPaths, + IHostingEnvironment hostingEnvironment, + UriUtility uriUtility, + ICookieManager cookieManager, + IHttpContextAccessor httpContextAccessor) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly UmbracoRequestPaths _umbracoRequestPaths; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ICookieManager _cookieManager; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly UriUtility _uriUtility; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoContextFactory( - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedSnapshotService publishedSnapshotService, - UmbracoRequestPaths umbracoRequestPaths, - IHostingEnvironment hostingEnvironment, - UriUtility uriUtility, - ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) - { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); - _umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility)); - _cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - } - - private IUmbracoContext CreateUmbracoContext() => new UmbracoContext( - _publishedSnapshotService, - _umbracoRequestPaths, - _hostingEnvironment, - _uriUtility, - _cookieManager, - _httpContextAccessor); - - /// - public UmbracoContextReference EnsureUmbracoContext() - { - if (_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return new UmbracoContextReference(umbracoContext, false, _umbracoContextAccessor); - } - - IUmbracoContext createdUmbracoContext = CreateUmbracoContext(); - - _umbracoContextAccessor.Set(createdUmbracoContext); - return new UmbracoContextReference(createdUmbracoContext, true, _umbracoContextAccessor); - } - + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedSnapshotService = publishedSnapshotService ?? + throw new ArgumentNullException(nameof(publishedSnapshotService)); + _umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility)); + _cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } + + /// + public UmbracoContextReference EnsureUmbracoContext() + { + if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return new UmbracoContextReference(umbracoContext, false, _umbracoContextAccessor); + } + + IUmbracoContext createdUmbracoContext = CreateUmbracoContext(); + + _umbracoContextAccessor.Set(createdUmbracoContext); + return new UmbracoContextReference(createdUmbracoContext, true, _umbracoContextAccessor); + } + + private IUmbracoContext CreateUmbracoContext() => new UmbracoContext( + _publishedSnapshotService, + _umbracoRequestPaths, + _hostingEnvironment, + _uriUtility, + _cookieManager, + _httpContextAccessor); } diff --git a/src/Umbraco.Web.Common/UmbracoHelper.cs b/src/Umbraco.Web.Common/UmbracoHelper.cs index 1b41a6b2fd..56181b29da 100644 --- a/src/Umbraco.Web.Common/UmbracoHelper.cs +++ b/src/Umbraco.Web.Common/UmbracoHelper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using System.Xml.XPath; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Dictionary; @@ -10,429 +7,400 @@ using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Xml; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common +namespace Umbraco.Cms.Web.Common; + +/// +/// A helper class that provides many useful methods and functionality for using Umbraco in templates +/// +/// +/// This object is a request based lifetime +/// +public class UmbracoHelper { + private readonly IUmbracoComponentRenderer _componentRenderer; + private readonly ICultureDictionaryFactory _cultureDictionaryFactory; + private readonly IPublishedContentQuery _publishedContentQuery; + private ICultureDictionary? _cultureDictionary; + + private IPublishedContent? _currentPage; + + #region Constructors + /// - /// A helper class that provides many useful methods and functionality for using Umbraco in templates + /// Initializes a new instance of . + /// + /// + /// + /// + /// Sets the current page to the context's published content request's content item. + public UmbracoHelper( + ICultureDictionaryFactory cultureDictionary, + IUmbracoComponentRenderer componentRenderer, + IPublishedContentQuery publishedContentQuery) + { + _cultureDictionaryFactory = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); + _componentRenderer = componentRenderer ?? throw new ArgumentNullException(nameof(componentRenderer)); + _publishedContentQuery = + publishedContentQuery ?? throw new ArgumentNullException(nameof(publishedContentQuery)); + } + + /// + /// Initializes a new empty instance of . + /// + /// For tests - nothing is initialized. +#pragma warning disable CS8618 + internal UmbracoHelper() +#pragma warning restore CS8618 + { + } + + #endregion + + /// + /// Gets (or sets) the current item assigned to the UmbracoHelper. /// /// - /// This object is a request based lifetime + /// + /// Note that this is the assigned IPublishedContent item to the + /// UmbracoHelper, this is not necessarily the Current IPublishedContent + /// item being rendered that is assigned to the UmbracoContext. + /// This IPublishedContent object is contextual to the current UmbracoHelper instance. + /// + /// + /// In some cases accessing this property will throw an exception if + /// there is not IPublishedContent assigned to the Helper this will + /// only ever happen if the Helper is constructed via DI during a non front-end request. + /// /// - public class UmbracoHelper + /// + /// Thrown if the + /// UmbracoHelper is constructed with an UmbracoContext and it is not a + /// front-end request. + /// + public IPublishedContent AssignedContentItem { - private readonly IPublishedContentQuery _publishedContentQuery; - private readonly IUmbracoComponentRenderer _componentRenderer; - private readonly ICultureDictionaryFactory _cultureDictionaryFactory; - - private IPublishedContent? _currentPage; - private ICultureDictionary? _cultureDictionary; - - #region Constructors - - /// - /// Initializes a new instance of . - /// - /// The item assigned to the helper. - /// - /// - /// - /// Sets the current page to the context's published content request's content item. - public UmbracoHelper(ICultureDictionaryFactory cultureDictionary, - IUmbracoComponentRenderer componentRenderer, - IPublishedContentQuery publishedContentQuery) + get { - _cultureDictionaryFactory = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); - _componentRenderer = componentRenderer ?? throw new ArgumentNullException(nameof(componentRenderer)); - _publishedContentQuery = publishedContentQuery ?? throw new ArgumentNullException(nameof(publishedContentQuery)); - } - - /// - /// Initializes a new empty instance of . - /// - /// For tests - nothing is initialized. -#pragma warning disable CS8618 - internal UmbracoHelper() -#pragma warning restore CS8618 - { } - - #endregion - - - /// - /// Gets (or sets) the current item assigned to the UmbracoHelper. - /// - /// - /// - /// Note that this is the assigned IPublishedContent item to the - /// UmbracoHelper, this is not necessarily the Current IPublishedContent - /// item being rendered that is assigned to the UmbracoContext. - /// This IPublishedContent object is contextual to the current UmbracoHelper instance. - /// - /// - /// In some cases accessing this property will throw an exception if - /// there is not IPublishedContent assigned to the Helper this will - /// only ever happen if the Helper is constructed via DI during a non front-end request. - /// - /// - /// Thrown if the - /// UmbracoHelper is constructed with an UmbracoContext and it is not a - /// front-end request. - public IPublishedContent AssignedContentItem - { - get + if (_currentPage != null) { - if (_currentPage != null) - { - return _currentPage; - } - - throw new InvalidOperationException( - $"Cannot return the {nameof(IPublishedContent)} because the {nameof(UmbracoHelper)} was not constructed with an {nameof(IPublishedContent)}." - ); - + return _currentPage; } - set => _currentPage = value; + + throw new InvalidOperationException( + $"Cannot return the {nameof(IPublishedContent)} because the {nameof(UmbracoHelper)} was not constructed with an {nameof(IPublishedContent)}."); } - - /// - /// Renders the template for the specified pageId and an optional altTemplateId - /// - /// The content id - /// If not specified, will use the template assigned to the node - public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) - => await _componentRenderer.RenderTemplateAsync(contentId, altTemplateId); - - #region RenderMacro - - /// - /// Renders the macro with the specified alias. - /// - /// The alias. - /// - public async Task RenderMacroAsync(string alias) - => await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, null); - - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The alias. - /// The parameters. - /// - public async Task RenderMacroAsync(string alias, object parameters) - => await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters?.ToDictionary()); - - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The alias. - /// The parameters. - /// - public async Task RenderMacroAsync(string alias, IDictionary parameters) - => await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters); - - #endregion - - #region Dictionary - - /// - /// Returns the dictionary value for the key specified - /// - /// - /// - public string? GetDictionaryValue(string key) - { - return CultureDictionary[key]; - } - - /// - /// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value - /// - /// key of dictionary item - /// fall back text if dictionary item is empty - Name altText to match Umbraco.Field - /// - public string GetDictionaryValue(string key, string altText) - { - var dictionaryValue = GetDictionaryValue(key); - if (String.IsNullOrWhiteSpace(dictionaryValue)) - { - dictionaryValue = altText; - } - return dictionaryValue; - } - - /// - /// Returns the ICultureDictionary for access to dictionary items - /// - public ICultureDictionary CultureDictionary => _cultureDictionary ??= _cultureDictionaryFactory.CreateDictionary(); - - #endregion - - - - #region Content - - /// - /// Gets a content item from the cache. - /// - /// The unique identifier, or the key, of the content item. - /// The content, or null of the content item is not in the cache. - public IPublishedContent? Content(object id) - { - return ContentForObject(id); - } - - private IPublishedContent? ContentForObject(object id) => _publishedContentQuery.Content(id); - - public IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars) - { - return _publishedContentQuery.ContentSingleAtXPath(xpath, vars); - } - - /// - /// Gets a content item from the cache. - /// - /// The unique identifier of the content item. - /// The content, or null of the content item is not in the cache. - public IPublishedContent? Content(int id) => _publishedContentQuery.Content(id); - - /// - /// Gets a content item from the cache. - /// - /// The key of the content item. - /// The content, or null of the content item is not in the cache. - public IPublishedContent? Content(Guid id) => _publishedContentQuery.Content(id); - - /// - /// Gets a content item from the cache. - /// - /// The unique identifier, or the key, of the content item. - /// The content, or null of the content item is not in the cache. - public IPublishedContent? Content(string id) => _publishedContentQuery.Content(id); - - public IPublishedContent? Content(Udi id) => _publishedContentQuery.Content(id); - - /// - /// Gets content items from the cache. - /// - /// The unique identifiers, or the keys, of the content items. - /// The content items that were found in the cache. - /// Does not support mixing identifiers and keys. - public IEnumerable Content(params object[] ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(params Udi[] ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(params GuidUdi[] ids) => _publishedContentQuery.Content(ids); - - private IEnumerable ContentForObjects(IEnumerable ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets content items from the cache. - /// - /// The unique identifiers of the content items. - /// The content items that were found in the cache. - public IEnumerable Content(params int[] ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets content items from the cache. - /// - /// The keys of the content items. - /// The content items that were found in the cache. - public IEnumerable Content(params Guid[] ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets content items from the cache. - /// - /// The unique identifiers, or the keys, of the content items. - /// The content items that were found in the cache. - /// Does not support mixing identifiers and keys. - public IEnumerable Content(params string[] ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); - - /// - /// Gets the contents corresponding to the identifiers. - /// - /// The content identifiers. - /// The existing contents corresponding to the identifiers. - /// If an identifier does not match an existing content, it will be missing in the returned value. - public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); - - public IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars) - { - return _publishedContentQuery.ContentAtXPath(xpath, vars); - } - - public IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars) - { - return _publishedContentQuery.ContentAtXPath(xpath, vars); - } - - public IEnumerable ContentAtRoot() - { - return _publishedContentQuery.ContentAtRoot(); - } - - - - - #endregion - #region Media - - public IPublishedContent? Media(Udi id) => _publishedContentQuery.Media(id); - - public IPublishedContent? Media(Guid id) => _publishedContentQuery.Media(id); - - /// - /// Overloaded method accepting an 'object' type - /// - /// - /// - /// - /// We accept an object type because GetPropertyValue now returns an 'object', we still want to allow people to pass - /// this result in to this method. - /// This method will throw an exception if the value is not of type int or string. - /// - public IPublishedContent? Media(object id) - { - return MediaForObject(id); - } - - private IPublishedContent? MediaForObject(object id) => _publishedContentQuery.Media(id); - - public IPublishedContent? Media(int id) => _publishedContentQuery.Media(id); - - public IPublishedContent? Media(string id) => _publishedContentQuery.Media(id); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(params object[] ids) => _publishedContentQuery.Media(ids); - - private IEnumerable MediaForObjects(IEnumerable ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(params int[] ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(params string[] ids) => _publishedContentQuery.Media(ids); - - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(params Udi[] ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(params GuidUdi[] ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); - - /// - /// Gets the medias corresponding to the identifiers. - /// - /// The media identifiers. - /// The existing medias corresponding to the identifiers. - /// If an identifier does not match an existing media, it will be missing in the returned value. - public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); - - public IEnumerable MediaAtRoot() - { - return _publishedContentQuery.MediaAtRoot(); - } - - #endregion + set => _currentPage = value; } + + /// + /// Renders the template for the specified pageId and an optional altTemplateId + /// + /// The content id + /// If not specified, will use the template assigned to the node + public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) + => await _componentRenderer.RenderTemplateAsync(contentId, altTemplateId); + + #region RenderMacro + + /// + /// Renders the macro with the specified alias. + /// + /// The alias. + /// + public async Task RenderMacroAsync(string alias) + => await _componentRenderer.RenderMacroAsync(AssignedContentItem.Id, alias, null); + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The alias. + /// The parameters. + /// + public async Task RenderMacroAsync(string alias, object parameters) + => await _componentRenderer.RenderMacroAsync(AssignedContentItem.Id, alias, parameters.ToDictionary()); + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The alias. + /// The parameters. + /// + public async Task RenderMacroAsync(string alias, IDictionary parameters) + => await _componentRenderer.RenderMacroAsync(AssignedContentItem.Id, alias, parameters); + + #endregion + + #region Dictionary + + /// + /// Returns the dictionary value for the key specified + /// + /// + /// + public string? GetDictionaryValue(string key) => CultureDictionary[key]; + + /// + /// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value + /// + /// key of dictionary item + /// fall back text if dictionary item is empty - Name altText to match Umbraco.Field + /// + public string GetDictionaryValue(string key, string altText) + { + var dictionaryValue = GetDictionaryValue(key); + if (string.IsNullOrWhiteSpace(dictionaryValue)) + { + dictionaryValue = altText; + } + + return dictionaryValue; + } + + /// + /// Returns the ICultureDictionary for access to dictionary items + /// + public ICultureDictionary CultureDictionary => _cultureDictionary ??= _cultureDictionaryFactory.CreateDictionary(); + + #endregion + + #region Content + + /// + /// Gets a content item from the cache. + /// + /// The unique identifier, or the key, of the content item. + /// The content, or null of the content item is not in the cache. + public IPublishedContent? Content(object id) => ContentForObject(id); + + private IPublishedContent? ContentForObject(object id) => _publishedContentQuery.Content(id); + + public IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars) => + _publishedContentQuery.ContentSingleAtXPath(xpath, vars); + + /// + /// Gets a content item from the cache. + /// + /// The unique identifier of the content item. + /// The content, or null of the content item is not in the cache. + public IPublishedContent? Content(int id) => _publishedContentQuery.Content(id); + + /// + /// Gets a content item from the cache. + /// + /// The key of the content item. + /// The content, or null of the content item is not in the cache. + public IPublishedContent? Content(Guid id) => _publishedContentQuery.Content(id); + + /// + /// Gets a content item from the cache. + /// + /// The unique identifier, or the key, of the content item. + /// The content, or null of the content item is not in the cache. + public IPublishedContent? Content(string id) => _publishedContentQuery.Content(id); + + public IPublishedContent? Content(Udi id) => _publishedContentQuery.Content(id); + + /// + /// Gets content items from the cache. + /// + /// The unique identifiers, or the keys, of the content items. + /// The content items that were found in the cache. + /// Does not support mixing identifiers and keys. + public IEnumerable Content(params object[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(params Udi[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(params GuidUdi[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets content items from the cache. + /// + /// The unique identifiers of the content items. + /// The content items that were found in the cache. + public IEnumerable Content(params int[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets content items from the cache. + /// + /// The keys of the content items. + /// The content items that were found in the cache. + public IEnumerable Content(params Guid[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets content items from the cache. + /// + /// The unique identifiers, or the keys, of the content items. + /// The content items that were found in the cache. + /// Does not support mixing identifiers and keys. + public IEnumerable Content(params string[] ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); + + /// + /// Gets the contents corresponding to the identifiers. + /// + /// The content identifiers. + /// The existing contents corresponding to the identifiers. + /// If an identifier does not match an existing content, it will be missing in the returned value. + public IEnumerable Content(IEnumerable ids) => _publishedContentQuery.Content(ids); + + public IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars) => + _publishedContentQuery.ContentAtXPath(xpath, vars); + + public IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars) => + _publishedContentQuery.ContentAtXPath(xpath, vars); + + public IEnumerable ContentAtRoot() => _publishedContentQuery.ContentAtRoot(); + + #endregion + + #region Media + + public IPublishedContent? Media(Udi id) => _publishedContentQuery.Media(id); + + public IPublishedContent? Media(Guid id) => _publishedContentQuery.Media(id); + + /// + /// Overloaded method accepting an 'object' type + /// + /// + /// + /// + /// We accept an object type because GetPropertyValue now returns an 'object', we still want to allow people to pass + /// this result in to this method. + /// This method will throw an exception if the value is not of type int or string. + /// + public IPublishedContent? Media(object id) => MediaForObject(id); + + private IPublishedContent? MediaForObject(object id) => _publishedContentQuery.Media(id); + + public IPublishedContent? Media(int id) => _publishedContentQuery.Media(id); + + public IPublishedContent? Media(string id) => _publishedContentQuery.Media(id); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(params object[] ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(params int[] ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(params string[] ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(params Udi[] ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(params GuidUdi[] ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); + + /// + /// Gets the medias corresponding to the identifiers. + /// + /// The media identifiers. + /// The existing medias corresponding to the identifiers. + /// If an identifier does not match an existing media, it will be missing in the returned value. + public IEnumerable Media(IEnumerable ids) => _publishedContentQuery.Media(ids); + + public IEnumerable MediaAtRoot() => _publishedContentQuery.MediaAtRoot(); + + #endregion } diff --git a/src/Umbraco.Web.Common/UmbracoHelperAccessor.cs b/src/Umbraco.Web.Common/UmbracoHelperAccessor.cs index 70f609d1c0..14e337e759 100644 --- a/src/Umbraco.Web.Common/UmbracoHelperAccessor.cs +++ b/src/Umbraco.Web.Common/UmbracoHelperAccessor.cs @@ -1,24 +1,19 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Web.Common +namespace Umbraco.Cms.Web.Common; + +public class UmbracoHelperAccessor : IUmbracoHelperAccessor { - public class UmbracoHelperAccessor : IUmbracoHelperAccessor + private readonly IHttpContextAccessor _httpContextAccessor; + + public UmbracoHelperAccessor(IHttpContextAccessor httpContextAccessor) => + _httpContextAccessor = httpContextAccessor; + + public bool TryGetUmbracoHelper([MaybeNullWhen(false)] out UmbracoHelper umbracoHelper) { - private readonly IHttpContextAccessor _httpContextAccessor; - - public UmbracoHelperAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - - public bool TryGetUmbracoHelper([MaybeNullWhen(false)] out UmbracoHelper umbracoHelper) - { - umbracoHelper = _httpContextAccessor.HttpContext?.RequestServices.GetService(); - return umbracoHelper is not null; - } + umbracoHelper = _httpContextAccessor.HttpContext?.RequestServices.GetService(); + return umbracoHelper is not null; } } diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index 7351a278fb..b1ac11c77d 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -17,242 +15,247 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Web.Common.Views +namespace Umbraco.Cms.Web.Common.Views; + +public abstract class UmbracoViewPage : UmbracoViewPage { - public abstract class UmbracoViewPage : UmbracoViewPage +} + +public abstract class UmbracoViewPage : RazorPage +{ + private UmbracoHelper? _helper; + + /// + /// Gets the Umbraco helper. + /// + public UmbracoHelper Umbraco { - - } - - public abstract class UmbracoViewPage : RazorPage - { - private IUmbracoContext? _umbracoContext; - private UmbracoHelper? _helper; - - private IUmbracoContextAccessor UmbracoContextAccessor => Context.RequestServices.GetRequiredService(); - - private GlobalSettings GlobalSettings => Context.RequestServices.GetRequiredService>().Value; - - private ContentSettings ContentSettings => Context.RequestServices.GetRequiredService>().Value; - - private IProfilerHtml ProfilerHtml => Context.RequestServices.GetRequiredService(); - - private IIOHelper IOHelper => Context.RequestServices.GetRequiredService(); - - - /// - /// Gets the Umbraco helper. - /// - public UmbracoHelper Umbraco + get { - get + if (_helper != null) { - if (_helper != null) - { - return _helper; - } - - TModel? model = ViewData.Model; - var content = model as IPublishedContent; - if (content is null && model is IContentModel contentModel) - { - content = contentModel.Content; - } - - if (content is null) - { - content = UmbracoContext?.PublishedRequest?.PublishedContent; - } - - _helper = Context.RequestServices.GetRequiredService(); - - if (!(content is null)) - { - _helper.AssignedContentItem = content; - } - return _helper; } - } - /// - /// Gets the - /// - protected IUmbracoContext? UmbracoContext - { - get + + TModel model = ViewData.Model; + var content = model as IPublishedContent; + if (content is null && model is IContentModel contentModel) { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + content = contentModel.Content; + } + + if (content is null) + { + content = UmbracoContext?.PublishedRequest?.PublishedContent; + } + + _helper = Context.RequestServices.GetRequiredService(); + + if (!(content is null)) + { + _helper.AssignedContentItem = content; + } + + return _helper; + } + } + + /// + public override ViewContext ViewContext + { + get => base.ViewContext; + set + { + // Here we do the magic model swap + ViewContext ctx = value; + ctx.ViewData = BindViewData( + ctx.HttpContext.RequestServices.GetRequiredService(), + ctx.ViewData); + base.ViewContext = ctx; + } + } + + /// + /// Gets the + /// + protected IUmbracoContext? UmbracoContext + { + get + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return null; + } + + return umbracoContext; + } + } + + private IUmbracoContextAccessor UmbracoContextAccessor => + Context.RequestServices.GetRequiredService(); + + private GlobalSettings GlobalSettings => + Context.RequestServices.GetRequiredService>().Value; + + private ContentSettings ContentSettings => + Context.RequestServices.GetRequiredService>().Value; + + private IProfilerHtml ProfilerHtml => Context.RequestServices.GetRequiredService(); + + private IHostingEnvironment HostingEnvironment => Context.RequestServices.GetRequiredService(); + + /// + public override void Write(object? value) + { + if (value is IHtmlEncodedString htmlEncodedString) + { + WriteLiteral(htmlEncodedString.ToHtmlString()); + } + else if (value is TagHelperOutput tagHelperOutput) + { + WriteUmbracoContent(tagHelperOutput); + base.Write(value); + } + else + { + base.Write(value); + } + } + + public void WriteUmbracoContent(TagHelperOutput tagHelperOutput) + { + // filter / add preview banner + // ASP.NET default value is text/html + if (Context.Response.ContentType.InvariantContains("text/html")) + { + if (((UmbracoContext?.IsDebug ?? false) || (UmbracoContext?.InPreviewMode ?? false)) + && tagHelperOutput.TagName != null + && tagHelperOutput.TagName.Equals("body", StringComparison.InvariantCultureIgnoreCase)) + { + string markupToInject; + + if (UmbracoContext.InPreviewMode) { - return null; + // creating previewBadge markup + markupToInject = + string.Format( + ContentSettings.PreviewBadge, + HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath), + Context.Request.GetEncodedUrl(), + UmbracoContext.PublishedRequest?.PublishedContent?.Id); + } + else + { + // creating mini-profiler markup + markupToInject = ProfilerHtml.Render(); } - return umbracoContext; + tagHelperOutput.Content.AppendHtml(markupToInject); } } + } - /// - public override ViewContext ViewContext + /// + /// Renders a section with default content if the section isn't defined + /// + public HtmlString? RenderSection(string name, string defaultContents) => + RazorPageExtensions.RenderSection(this, name, defaultContents); + + /// + /// Renders a section with default content if the section isn't defined + /// + public HtmlString? RenderSection(string name, HtmlString defaultContents) => + RazorPageExtensions.RenderSection(this, name, defaultContents); + + /// + /// Dynamically binds the incoming to the required + /// + /// + /// + /// This is used in order to provide the ability for an Umbraco view to either have a model of type + /// or . This will use the + /// to bind the models + /// to the correct output type. + /// + protected ViewDataDictionary BindViewData(ContentModelBinder contentModelBinder, ViewDataDictionary? viewData) + { + if (contentModelBinder is null) { - get => base.ViewContext; - set - { - // Here we do the magic model swap - ViewContext ctx = value; - ctx.ViewData = BindViewData(ctx.HttpContext.RequestServices.GetRequiredService(), ctx.ViewData); - base.ViewContext = ctx; - } + throw new ArgumentNullException(nameof(contentModelBinder)); } - /// - public override void Write(object? value) + if (viewData is null) { - if (value is IHtmlEncodedString htmlEncodedString) - { - WriteLiteral(htmlEncodedString.ToHtmlString()); - } - else if (value is TagHelperOutput tagHelperOutput) - { - WriteUmbracoContent(tagHelperOutput); - base.Write(value); - } - else - { - base.Write(value); - } + throw new ArgumentNullException(nameof(viewData)); } - /// - public void WriteUmbracoContent(TagHelperOutput tagHelperOutput) + // check if it's already the correct type and continue if it is + if (viewData is ViewDataDictionary vdd) { - // filter / add preview banner - // ASP.NET default value is text/html - if (Context.Response?.ContentType?.InvariantContains("text/html") ?? false) - { - if (((UmbracoContext?.IsDebug ?? false) || (UmbracoContext?.InPreviewMode ?? false)) - && tagHelperOutput.TagName != null - && tagHelperOutput.TagName.Equals("body", StringComparison.InvariantCultureIgnoreCase)) - { - string markupToInject; - - if (UmbracoContext.InPreviewMode) - { - // creating previewBadge markup - markupToInject = - string.Format( - ContentSettings.PreviewBadge, - IOHelper.ResolveUrl(GlobalSettings.UmbracoPath), - Context.Request.GetEncodedUrl(), - UmbracoContext.PublishedRequest?.PublishedContent?.Id); - } - else - { - // creating mini-profiler markup - markupToInject = ProfilerHtml.Render(); - } - - tagHelperOutput.Content.AppendHtml(markupToInject); - } - } + return vdd; } - /// - /// Dynamically binds the incoming to the required - /// - /// - /// This is used in order to provide the ability for an Umbraco view to either have a model of type - /// or . This will use the to bind the models - /// to the correct output type. - /// - protected ViewDataDictionary BindViewData(ContentModelBinder contentModelBinder, ViewDataDictionary? viewData) + // Here we hand the default case where we know the incoming model is ContentModel and the + // outgoing model is IPublishedContent. This is a fast conversion that doesn't require doing the full + // model binding, allocating classes, etc... + if (viewData.ModelMetadata.ModelType == typeof(ContentModel) + && typeof(TModel) == typeof(IPublishedContent)) { - if (contentModelBinder is null) + var contentModel = (ContentModel?)viewData.Model; + viewData.Model = contentModel?.Content; + return viewData; + } + + // capture the model before we tinker with the viewData + var viewDataModel = viewData.Model; + + // map the view data (may change its type, may set model to null) + viewData = MapViewDataDictionary(viewData, typeof(TModel)); + + // bind the model + var bindingContext = new DefaultModelBindingContext(); + contentModelBinder.BindModel(bindingContext, viewDataModel, typeof(TModel)); + + viewData!.Model = bindingContext.Result.Model; + + // return the new view data + return (ViewDataDictionary)viewData; + } + + // viewData is the ViewDataDictionary (maybe ) that we have + // modelType is the type of the model that we need to bind to + // figure out whether viewData can accept modelType else replace it + private static ViewDataDictionary? MapViewDataDictionary(ViewDataDictionary viewData, Type modelType) + { + Type viewDataType = viewData.GetType(); + + if (viewDataType.IsGenericType) + { + // ensure it is the proper generic type + Type def = viewDataType.GetGenericTypeDefinition(); + if (def != typeof(ViewDataDictionary<>)) { - throw new ArgumentNullException(nameof(contentModelBinder)); + throw new Exception("Could not map viewData of type \"" + viewDataType.FullName + "\"."); } - if (viewData is null) - { - throw new ArgumentNullException(nameof(viewData)); - } + // get the viewData model type and compare with the actual view model type: + // viewData is ViewDataDictionary and we will want to assign an + // object of type modelType to the Model property of type viewDataModelType, we + // need to check whether that is possible + Type viewDataModelType = viewDataType.GenericTypeArguments[0]; - // check if it's already the correct type and continue if it is - if (viewData is ViewDataDictionary vdd) + if (viewDataModelType != typeof(object) && viewDataModelType.IsAssignableFrom(modelType)) { - return vdd; - } - - // Here we hand the default case where we know the incoming model is ContentModel and the - // outgoing model is IPublishedContent. This is a fast conversion that doesn't require doing the full - // model binding, allocating classes, etc... - if (viewData.ModelMetadata.ModelType == typeof(ContentModel) - && typeof(TModel) == typeof(IPublishedContent)) - { - var contentModel = (ContentModel?)viewData.Model; - viewData.Model = contentModel?.Content; return viewData; } - - // capture the model before we tinker with the viewData - var viewDataModel = viewData.Model; - - // map the view data (may change its type, may set model to null) - viewData = MapViewDataDictionary(viewData, typeof(TModel)); - - // bind the model - var bindingContext = new DefaultModelBindingContext(); - contentModelBinder.BindModel(bindingContext, viewDataModel, typeof(TModel)); - - viewData!.Model = bindingContext.Result.Model; - - // return the new view data - return (ViewDataDictionary)viewData; } - // viewData is the ViewDataDictionary (maybe ) that we have - // modelType is the type of the model that we need to bind to - // figure out whether viewData can accept modelType else replace it - private static ViewDataDictionary? MapViewDataDictionary(ViewDataDictionary viewData, Type modelType) - { - Type viewDataType = viewData.GetType(); - - if (viewDataType.IsGenericType) - { - // ensure it is the proper generic type - Type def = viewDataType.GetGenericTypeDefinition(); - if (def != typeof(ViewDataDictionary<>)) - { - throw new Exception("Could not map viewData of type \"" + viewDataType.FullName + "\"."); - } - - // get the viewData model type and compare with the actual view model type: - // viewData is ViewDataDictionary and we will want to assign an - // object of type modelType to the Model property of type viewDataModelType, we - // need to check whether that is possible - Type viewDataModelType = viewDataType.GenericTypeArguments[0]; - - if (viewDataModelType != typeof(object) && viewDataModelType.IsAssignableFrom(modelType)) - { - return viewData; - } - } - - // if not possible or it is not generic then we need to create a new ViewDataDictionary - Type nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); - var tViewData = new ViewDataDictionary(viewData) { Model = default(TModel) }; // temp view data to copy values - var nViewData = (ViewDataDictionary?)Activator.CreateInstance(nViewDataType, tViewData); - return nViewData; - } - - /// - /// Renders a section with default content if the section isn't defined - /// - public HtmlString? RenderSection(string name, HtmlString defaultContents) => RazorPageExtensions.RenderSection(this, name, defaultContents); - - /// - /// Renders a section with default content if the section isn't defined - /// - public HtmlString? RenderSection(string name, string defaultContents) => RazorPageExtensions.RenderSection(this, name, defaultContents); - + // if not possible or it is not generic then we need to create a new ViewDataDictionary + Type nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + var tViewData = new ViewDataDictionary(viewData) { Model = default(TModel) }; // temp view data to copy values + var nViewData = (ViewDataDictionary?)Activator.CreateInstance(nViewDataType, tViewData); + return nViewData; } }