Add DynamicRequestCultureProviderBase and improve locking (#14064)

This commit is contained in:
Ronald Barendse
2023-05-02 09:46:43 +02:00
committed by Bjarke Berg
parent d3243f8700
commit b743e715d4
5 changed files with 150 additions and 106 deletions

View File

@@ -7,26 +7,56 @@ using System.Security.Principal;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for <see cref="IIdentity" />.
/// </summary>
public static class AuthenticationExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="identity">The identity.</param>
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;
}
}
/// <summary>
/// Gets the culture string from the back office user.
/// </summary>
/// <param name="identity">The identity.</param>
/// <returns>
/// The culture string.
/// </returns>
public static string? GetCultureString(this IIdentity identity)
{
if (identity is ClaimsIdentity umbIdentity &&
umbIdentity.VerifyBackOfficeIdentity(out _) &&
umbIdentity.IsAuthenticated)
{
return umbIdentity.GetCultureString();
}
return null;
}
/// <summary>
/// Gets the culture from the back office user.
/// </summary>
/// <param name="identity">The identity.</param>
/// <returns>
/// The culture.
/// </returns>
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;

View File

@@ -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;
/// <summary>
/// Base implementation that dynamically adds the determined cultures to the supported cultures.
/// </summary>
public abstract class DynamicRequestCultureProviderBase : RequestCultureProvider
{
private readonly RequestLocalizationOptions _options;
private readonly object _lockerSupportedCultures = new();
private readonly object _lockerSupportedUICultures = new();
/// <summary>
/// Initializes a new instance of the <see cref="DynamicRequestCultureProviderBase" /> class.
/// </summary>
/// <param name="requestLocalizationOptions">The request localization options.</param>
protected DynamicRequestCultureProviderBase(RequestLocalizationOptions requestLocalizationOptions)
=> _options = Options = requestLocalizationOptions;
/// <inheritdoc />
public override Task<ProviderCultureResult?> 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<CultureInfo>();
_options.SupportedCultures.Add(CultureInfo.GetCultureInfo(culture.ToString()));
});
TryAddLocked(_options.SupportedUICultures, result.UICultures, _lockerSupportedUICultures, (culture) =>
{
_options.SupportedUICultures ??= new List<CultureInfo>();
_options.SupportedUICultures.Add(CultureInfo.GetCultureInfo(culture.ToString()));
});
return Task.FromResult<ProviderCultureResult?>(result);
}
return NullProviderCultureResult;
}
/// <summary>
/// Gets the provider culture result.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>
/// The provider culture result.
/// </returns>
protected abstract ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext);
/// <summary>
/// Executes the <paramref name="addAction" /> within a double-checked lock when the a culture in <paramref name="cultures" /> does not exist in <paramref name="supportedCultures" />.
/// </summary>
/// <param name="supportedCultures">The supported cultures.</param>
/// <param name="cultures">The cultures.</param>
/// <param name="locker">The locker object to use.</param>
/// <param name="addAction">The add action to execute.</param>
private static void TryAddLocked(IEnumerable<CultureInfo>? supportedCultures, IEnumerable<StringSegment> cultures, object locker, Action<StringSegment> addAction)
{
foreach (StringSegment culture in cultures)
{
Func<CultureInfo, bool> 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);
}
}
}
}
}
}

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class UmbracoBackOfficeIdentityCultureProvider : RequestCultureProvider
public class UmbracoBackOfficeIdentityCultureProvider : DynamicRequestCultureProviderBase
{
private readonly RequestLocalizationOptions _localizationOptions;
private readonly object _locker = new();
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoBackOfficeIdentityCultureProvider" /> class.
/// Initializes a new instance of the <see cref="UmbracoBackOfficeIdentityCultureProvider" /> class.
/// </summary>
public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions) =>
_localizationOptions = localizationOptions;
/// <param name="localizationOptions">The localization options.</param>
public UmbracoBackOfficeIdentityCultureProvider(RequestLocalizationOptions localizationOptions)
: base(localizationOptions)
{ }
/// <inheritdoc />
public override Task<ProviderCultureResult?> 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<ProviderCultureResult?>(new ProviderCultureResult(culture.Name));
}
}
protected sealed override ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext)
=> httpContext.User.Identity?.GetCultureString() is string culture
? new ProviderCultureResult(culture)
: null;
}

View File

@@ -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;
/// <summary>
/// Sets the request culture to the culture of the <see cref="IPublishedRequest" /> if one is found in the request
/// Sets the request culture to the culture of the <see cref="IPublishedRequest" />, if one is found in the request.
/// </summary>
public class UmbracoPublishedContentCultureProvider : RequestCultureProvider
public class UmbracoPublishedContentCultureProvider : DynamicRequestCultureProviderBase
{
private readonly RequestLocalizationOptions _localizationOptions;
private readonly object _locker = new();
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoPublishedContentCultureProvider" /> class.
/// Initializes a new instance of the <see cref="UmbracoPublishedContentCultureProvider" /> class.
/// </summary>
public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions) =>
_localizationOptions = localizationOptions;
/// <param name="localizationOptions">The localization options.</param>
public UmbracoPublishedContentCultureProvider(RequestLocalizationOptions localizationOptions)
: base(localizationOptions)
{ }
/// <inheritdoc />
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
UmbracoRouteValues? routeValues = httpContext.Features.Get<UmbracoRouteValues>();
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<ProviderCultureResult?>(new ProviderCultureResult(culture));
}
}
return NullProviderCultureResult;
}
protected sealed override ProviderCultureResult? GetProviderCultureResult(HttpContext httpContext)
=> httpContext.Features.Get<UmbracoRouteValues>()?.PublishedRequest.Culture is string culture
? new ProviderCultureResult(culture)
: null;
}

View File

@@ -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;
/// <summary>
/// Custom Umbraco options configuration for <see cref="RequestLocalizationOptions" />
/// Custom Umbraco options configuration for <see cref="RequestLocalizationOptions" />.
/// </summary>
public class UmbracoRequestLocalizationOptions : IConfigureOptions<RequestLocalizationOptions>
{
private readonly GlobalSettings _globalSettings;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRequestLocalizationOptions" /> class.
/// Initializes a new instance of the <see cref="UmbracoRequestLocalizationOptions" /> class.
/// </summary>
public UmbracoRequestLocalizationOptions(IOptions<GlobalSettings> globalSettings) =>
_globalSettings = globalSettings.Value;
/// <param name="globalSettings">The global settings.</param>
public UmbracoRequestLocalizationOptions(IOptions<GlobalSettings> globalSettings)
=> _globalSettings = globalSettings.Value;
/// <inheritdoc />
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));