From 1dac8779c2fdaaface2256ec723f1005d77dee33 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 4 Aug 2020 12:54:54 +0200 Subject: [PATCH 1/2] https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/7619 - Added request localization from the current user --- .../Security/AuthenticationExtensions.cs | 14 +++++++-- ...mbracoBackOfficeIdentityCultureProvider.cs | 22 ++++++++++++++ .../UmbracoCoreServiceCollectionExtensions.cs | 29 +++++++++++++++++-- src/Umbraco.Web.UI.NetCore/Startup.cs | 2 +- 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index d0b4416eed..edc11bcac2 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -16,12 +16,22 @@ namespace Umbraco.Core.Security /// /// public static void EnsureCulture(this IIdentity identity) + { + var culture = GetCulture(identity); + if (!(culture is null)) + { + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; + } + } + + public static CultureInfo GetCulture(this IIdentity identity) { if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated) { - Thread.CurrentThread.CurrentUICulture = - Thread.CurrentThread.CurrentCulture = UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s)); + return UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s)); } + + return null; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs new file mode 100644 index 0000000000..a5af18fbda --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Umbraco.Core.Security; + +namespace Umbraco.Web.Common.Extensions +{ + public class UmbracoBackOfficeIdentityCultureProvider : RequestCultureProvider + { + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + var culture = httpContext.User.Identity.GetCulture(); + + if (culture is null) + { + return NullProviderCultureResult; + } + + return Task.FromResult(new ProviderCultureResult(culture.Name, culture.Name)); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 358378ca35..81d3241aa5 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -1,10 +1,12 @@ using System; -using System.Collections; using System.Data.Common; using System.Data.SqlClient; +using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -13,7 +15,6 @@ using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Hosting; using Serilog.Extensions.Logging; -using Umbraco.Composing; using Umbraco.Configuration; using Umbraco.Core; using Umbraco.Core.Cache; @@ -26,6 +27,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Runtime; using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Extensions; using Umbraco.Web.Common.Profiler; namespace Umbraco.Extensions @@ -156,6 +158,23 @@ namespace Umbraco.Extensions return services; } + public static IServiceCollection AddUmbracoRequestLocalization(this IServiceCollection services) + { + services.Configure(options => + { + var supportedCultures = CultureInfo + .GetCultures(CultureTypes.AllCultures & ~ CultureTypes.NeutralCultures) + .Where(cul => !String.IsNullOrEmpty(cul.Name)) + .ToArray(); + + options.SupportedCultures = supportedCultures; + options.SupportedUICultures = supportedCultures; + options.AddInitialRequestCultureProvider(new UmbracoBackOfficeIdentityCultureProvider()); + }); + + return services; + } + /// /// Adds the Umbraco Back Core requirements /// @@ -182,6 +201,10 @@ namespace Umbraco.Extensions if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); + // Set culture options + services.AddUmbracoRequestLocalization(); + + // Add supported databases services.AddUmbracoSqlCeSupport(); services.AddUmbracoSqlServerSupport(); @@ -228,7 +251,7 @@ namespace Umbraco.Extensions factory = coreRuntime.Configure(container); return services; - } + } private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly, ITypeFinderSettings typeFinderSettings) { diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index cd4fa3eaac..7ccbdd6ab4 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; -using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.UI.BackOffice { @@ -80,6 +79,7 @@ namespace Umbraco.Web.UI.BackOffice app.UseUmbracoCore(); app.UseUmbracoRouting(); + app.UseRequestLocalization(); app.UseUmbracoRequestLogging(); app.UseUmbracoWebsite(); app.UseUmbracoBackOffice(); From 1239975b076e131f4aac32740e30ef6176519055 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Aug 2020 22:08:27 +1000 Subject: [PATCH 2/2] Ensures all back office controllers have the thread culture set, we do this with a new application model targeting all back office controllers. --- .../BackOfficeApplicationModelProvider.cs | 58 +++++++++++++++++++ .../BackOfficeIdentityCultureConvention.cs | 13 +++++ ...racoApiBehaviorApplicationModelProvider.cs | 2 +- .../UmbracoCoreServiceCollectionExtensions.cs | 22 +------ .../UmbracoWebServiceCollectionExtensions.cs | 1 + .../Filters/BackOfficeCultureFilter.cs | 34 +++++++++++ 6 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs create mode 100644 src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs create mode 100644 src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs new file mode 100644 index 0000000000..d7c9833c6f --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Web.Common.Attributes; + +namespace Umbraco.Web.Common.ApplicationModels +{ + /// + /// An application model provider for all Umbraco Back Office controllers + /// + public class BackOfficeApplicationModelProvider : IApplicationModelProvider + { + public BackOfficeApplicationModelProvider(IModelMetadataProvider modelMetadataProvider) + { + ActionModelConventions = new List() + { + new BackOfficeIdentityCultureConvention() + }; + } + + /// + /// Will execute after + /// + public int Order => 0; + + public List ActionModelConventions { get; } + + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (var controller in context.Result.Controllers) + { + if (!IsBackOfficeController(controller)) + continue; + + foreach (var action in controller.Actions) + { + foreach (var convention in ActionModelConventions) + { + convention.Apply(action); + } + } + + } + } + + private bool IsBackOfficeController(ControllerModel controller) + { + var pluginControllerAttribute = controller.Attributes.OfType().FirstOrDefault(); + return pluginControllerAttribute != null + && pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeArea; + } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs new file mode 100644 index 0000000000..d3e2096dd3 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Umbraco.Web.Common.Filters; + +namespace Umbraco.Web.Common.ApplicationModels +{ + public class BackOfficeIdentityCultureConvention : IActionModelConvention + { + 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 e76ae1ff6b..918bc3776f 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Common.ApplicationModels { /// - /// A custom application model provider for Umbraco controllers + /// An application model provider for Umbraco API controllers to behave like WebApi controllers /// /// /// diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 81d3241aa5..91cee9672a 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -156,24 +156,7 @@ namespace Umbraco.Extensions out factory); return services; - } - - public static IServiceCollection AddUmbracoRequestLocalization(this IServiceCollection services) - { - services.Configure(options => - { - var supportedCultures = CultureInfo - .GetCultures(CultureTypes.AllCultures & ~ CultureTypes.NeutralCultures) - .Where(cul => !String.IsNullOrEmpty(cul.Name)) - .ToArray(); - - options.SupportedCultures = supportedCultures; - options.SupportedUICultures = supportedCultures; - options.AddInitialRequestCultureProvider(new UmbracoBackOfficeIdentityCultureProvider()); - }); - - return services; - } + } /// /// Adds the Umbraco Back Core requirements @@ -201,9 +184,6 @@ namespace Umbraco.Extensions if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); - // Set culture options - services.AddUmbracoRequestLocalization(); - // Add supported databases services.AddUmbracoSqlCeSupport(); services.AddUmbracoSqlServerSupport(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs index a3e9b901dc..a795edf6cf 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs @@ -36,6 +36,7 @@ namespace Umbraco.Extensions { services.ConfigureOptions(); services.TryAddEnumerable(ServiceDescriptor.Transient()); + services.TryAddEnumerable(ServiceDescriptor.Transient()); // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured var serviceProvider = services.BuildServiceProvider(); diff --git a/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs b/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs new file mode 100644 index 0000000000..99109fe230 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/BackOfficeCultureFilter.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core.Security; +using System.Globalization; + +namespace Umbraco.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 + { + public void OnActionExecuted(ActionExecutedContext context) + { + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var culture = context.HttpContext.User.Identity.GetCulture(); + if (culture != null) + { + SetCurrentThreadCulture(culture); + } + } + + private static void SetCurrentThreadCulture(CultureInfo culture) + { + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } + } + + +}