Add DynamicRequestCultureProviderBase and improve locking (#14064)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user