diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 2b8294e8db..7e66a05b2e 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -7,26 +7,56 @@ using System.Security.Principal; namespace Umbraco.Extensions; +/// +/// Extension methods for . +/// public static class AuthenticationExtensions { /// - /// Ensures that the thread culture is set based on the back office user's culture + /// Ensures that the thread culture is set based on the back office user's culture. /// + /// The identity. public static void EnsureCulture(this IIdentity identity) { CultureInfo? culture = GetCulture(identity); - if (!(culture is null)) + if (culture is not null) { Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; } } + /// + /// Gets the culture string from the back office user. + /// + /// The identity. + /// + /// The culture string. + /// + public static string? GetCultureString(this IIdentity identity) + { + if (identity is ClaimsIdentity umbIdentity && + umbIdentity.VerifyBackOfficeIdentity(out _) && + umbIdentity.IsAuthenticated) + { + return umbIdentity.GetCultureString(); + } + + return null; + } + + /// + /// Gets the culture from the back office user. + /// + /// The identity. + /// + /// The culture. + /// public static CultureInfo? GetCulture(this IIdentity identity) { - if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && - umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) + string? culture = identity.GetCultureString(); + if (!string.IsNullOrEmpty(culture)) { - return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); + return CultureInfo.GetCultureInfo(culture); } return null; diff --git a/src/Umbraco.Web.Common/Localization/DynamicRequestCultureProviderBase.cs b/src/Umbraco.Web.Common/Localization/DynamicRequestCultureProviderBase.cs new file mode 100644 index 0000000000..84bf1531b7 --- /dev/null +++ b/src/Umbraco.Web.Common/Localization/DynamicRequestCultureProviderBase.cs @@ -0,0 +1,86 @@ +using System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Primitives; + +namespace Umbraco.Cms.Web.Common.Localization; + +/// +/// Base implementation that dynamically adds the determined cultures to the supported cultures. +/// +public abstract class DynamicRequestCultureProviderBase : RequestCultureProvider +{ + private readonly RequestLocalizationOptions _options; + private readonly object _lockerSupportedCultures = new(); + private readonly object _lockerSupportedUICultures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The request localization options. + protected DynamicRequestCultureProviderBase(RequestLocalizationOptions requestLocalizationOptions) + => _options = Options = requestLocalizationOptions; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + ProviderCultureResult? result = GetProviderCultureResult(httpContext); + if (result is not 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 + TryAddLocked(_options.SupportedCultures, result.Cultures, _lockerSupportedCultures, (culture) => + { + _options.SupportedCultures ??= new List(); + _options.SupportedCultures.Add(CultureInfo.GetCultureInfo(culture.ToString())); + }); + + TryAddLocked(_options.SupportedUICultures, result.UICultures, _lockerSupportedUICultures, (culture) => + { + _options.SupportedUICultures ??= new List(); + _options.SupportedUICultures.Add(CultureInfo.GetCultureInfo(culture.ToString())); + }); + + return Task.FromResult(result); + } + + return NullProviderCultureResult; + } + + /// + /// Gets the provider culture result. + /// + /// The HTTP context. + /// + /// The provider culture result. + /// + protected abstract ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext); + + /// + /// Executes the within a double-checked lock when the a culture in does not exist in . + /// + /// The supported cultures. + /// The cultures. + /// The locker object to use. + /// The add action to execute. + private static void TryAddLocked(IEnumerable? supportedCultures, IEnumerable cultures, object locker, Action addAction) + { + foreach (StringSegment culture in cultures) + { + Func predicate = x => culture.Equals(x.Name, StringComparison.OrdinalIgnoreCase); + if (supportedCultures?.Any(predicate) is not true) + { + lock (locker) + { + if (supportedCultures?.Any(predicate) is not true) + { + addAction(culture); + } + } + } + } + } +} diff --git a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs index 3e84774c7b..95b5a8d13e 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Globalization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; @@ -10,50 +9,21 @@ using Umbraco.Extensions; 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 +/// 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 +public class UmbracoBackOfficeIdentityCultureProvider : DynamicRequestCultureProviderBase { - private readonly RequestLocalizationOptions _localizationOptions; - private readonly object _locker = new(); - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions) => - _localizationOptions = localizationOptions; + /// The localization options. + public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions) + : base(localizationOptions) + { } /// - public override Task DetermineProviderCultureResult(HttpContext httpContext) - { - CultureInfo? culture = httpContext.User.Identity?.GetCulture(); - - if (culture is null) - { - return NullProviderCultureResult; - } - - 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) - { - // 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)); - } - } + protected sealed override ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext) + => httpContext.User.Identity?.GetCultureString() is string culture + ? new ProviderCultureResult(culture) + : null; } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs index a3252c66e6..c2ad4805c0 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs @@ -1,69 +1,27 @@ -using System.Globalization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; -using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; namespace Umbraco.Cms.Web.Common.Localization; /// -/// Sets the request culture to the culture of the if one is found in the request +/// Sets the request culture to the culture of the , if one is found in the request. /// -public class UmbracoPublishedContentCultureProvider : RequestCultureProvider +public class UmbracoPublishedContentCultureProvider : DynamicRequestCultureProviderBase { - private readonly RequestLocalizationOptions _localizationOptions; - private readonly object _locker = new(); - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions) => - _localizationOptions = localizationOptions; + /// The localization options. + public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions) + : base(localizationOptions) + { } /// - public override Task DetermineProviderCultureResult(HttpContext httpContext) - { - UmbracoRouteValues? routeValues = httpContext.Features.Get(); - if (routeValues != null) - { - var culture = routeValues.PublishedRequest.Culture; - if (culture != 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 - // 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); - } - } - - return Task.FromResult(new ProviderCultureResult(culture)); - } - } - - return NullProviderCultureResult; - } + protected sealed override ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext) + => httpContext.Features.Get()?.PublishedRequest.Culture is string culture + ? new ProviderCultureResult(culture) + : null; } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs index 802a68607d..59c11dc986 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs @@ -1,28 +1,28 @@ using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Web.Common.Localization; /// -/// Custom Umbraco options configuration for +/// Custom Umbraco options configuration for . /// public class UmbracoRequestLocalizationOptions : IConfigureOptions { private readonly GlobalSettings _globalSettings; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public UmbracoRequestLocalizationOptions(IOptions globalSettings) => - _globalSettings = globalSettings.Value; + /// The global settings. + 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); + // Set the default culture to what is in config + options.SetDefaultCulture(_globalSettings.DefaultUILanguage); options.RequestCultureProviders.Insert(0, new UmbracoBackOfficeIdentityCultureProvider(options)); options.RequestCultureProviders.Insert(1, new UmbracoPublishedContentCultureProvider(options));