From c15b416e889efbd24b8bf72d09f1866d2db3da2e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Jan 2021 11:29:07 +1100 Subject: [PATCH] Ensures the culture is set for requests for both front-end and back office --- src/Umbraco.Core/Routing/PublishedRouter.cs | 4 -- .../Routing/UmbracoModuleTests.cs | 4 +- .../Controllers/RenderController.cs | 14 ++++--- .../UmbracoBuilderExtensions.cs | 3 ++ .../ApplicationBuilderExtensions.cs | 4 +- ...mbracoBackOfficeIdentityCultureProvider.cs | 12 ++++-- .../UmbracoPublishedContentCultureProvider.cs | 31 ++++++++++++++++ .../UmbracoRequestLocalizationOptions.cs | 37 +++++++++++++++++++ .../UmbracoMvcConfigureOptions.cs | 3 +- src/Umbraco.Web/UmbracoInjectedModule.cs | 6 --- 10 files changed, 94 insertions(+), 24 deletions(-) rename src/Umbraco.Web.Common/{Extensions => Localization}/UmbracoBackOfficeIdentityCultureProvider.cs (61%) create mode 100644 src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs create mode 100644 src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs rename src/Umbraco.Web.Common/{AspNetCore => Mvc}/UmbracoMvcConfigureOptions.cs (95%) diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index c17eb3e2b7..b61baa1990 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -123,10 +123,6 @@ namespace Umbraco.Web.Routing private void SetVariationContext(CultureInfo culture) { - // set the culture on the thread - once, so it's set when running document lookups - // TODO: Set this on HttpContext! - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; - VariationContext variationContext = _variationContextAccessor.VariationContext; if (variationContext != null && variationContext.Culture == culture?.Name) { diff --git a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs index 267d870514..dbcedb6225 100644 --- a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs +++ b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using Microsoft.Extensions.Logging; using Moq; @@ -35,8 +35,6 @@ namespace Umbraco.Tests.Routing null, // FIXME: PublishedRouter complexities... Mock.Of(), new RoutableDocumentFilter(globalSettings, IOHelper), - UriUtility, - AppCaches.RequestCache, globalSettings, HostingEnvironment ); diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 1556333402..d9a2d05979 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Common.ActionsResults; @@ -70,12 +71,13 @@ namespace Umbraco.Web.Common.Controllers return _umbracoRouteValues; } - if (!ControllerContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var def)) + _umbracoRouteValues = HttpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) as UmbracoRouteValues; + + if (_umbracoRouteValues == null) { throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}"); } - _umbracoRouteValues = (UmbracoRouteValues)def; return _umbracoRouteValues; } } @@ -126,10 +128,10 @@ namespace Umbraco.Web.Common.Controllers IPublishedRequest pcr = UmbracoRouteValues.PublishedRequest; _logger.LogDebug( - "Response status: Redirect={Redirect}, Is404={Is404}, StatusCode={ResponseStatusCode}", - pcr.IsRedirect() ? (pcr.IsRedirectPermanent() ? "permanent" : "redirect") : "none", - pcr.Is404() ? "true" : "false", - pcr.ResponseStatusCode); + "Response status: Content={Content}, StatusCode={ResponseStatusCode}, Culture={Culture}", + pcr.PublishedContent?.Id ?? -1, + pcr.ResponseStatusCode, + pcr.Culture); UmbracoRouteResult routeStatus = pcr.GetRouteResult(); switch (routeStatus) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index a2dde620b9..8a6f44f456 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -48,9 +48,11 @@ using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.DependencyInjection; using Umbraco.Web.Common.Install; using Umbraco.Web.Common.Lifetime; +using Umbraco.Web.Common.Localization; using Umbraco.Web.Common.Macros; using Umbraco.Web.Common.Middleware; using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Common.Mvc; using Umbraco.Web.Common.Profiler; using Umbraco.Web.Common.Routing; using Umbraco.Web.Common.Security; @@ -226,6 +228,7 @@ namespace Umbraco.Web.Common.DependencyInjection }); builder.Services.ConfigureOptions(); + builder.Services.ConfigureOptions(); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.AddUmbracoImageSharp(builder.Config); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 5dee7d10e1..73682ca6c5 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; @@ -57,9 +58,10 @@ namespace Umbraco.Extensions // where we need to have UseAuthentication and UseAuthorization proceeding this call but before // endpoints are defined. app.UseRouting(); - app.UseRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); + // This must come after auth because the culture is based on the auth'd user + app.UseRequestLocalization(); // Must be called after UseRouting and before UseEndpoints app.UseSession(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs similarity index 61% rename from src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs rename to src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs index a5af18fbda..a09230a3fc 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoBackOfficeIdentityCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs @@ -1,22 +1,28 @@ +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Umbraco.Core.Security; -namespace Umbraco.Web.Common.Extensions +namespace Umbraco.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 { + /// public override Task DetermineProviderCultureResult(HttpContext httpContext) { - var culture = httpContext.User.Identity.GetCulture(); + CultureInfo culture = httpContext.User.Identity.GetCulture(); if (culture is null) { return NullProviderCultureResult; } - return Task.FromResult(new ProviderCultureResult(culture.Name, culture.Name)); + 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 new file mode 100644 index 0000000000..cc683848c3 --- /dev/null +++ b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Routing; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.Localization +{ + /// + /// Sets the request culture to the culture of the if one is found in the request + /// + public class UmbracoPublishedContentCultureProvider : RequestCultureProvider + { + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) is UmbracoRouteValues routeValues) + { + CultureInfo culture = routeValues.PublishedRequest?.Culture; + if (culture != null) + { + return Task.FromResult(new ProviderCultureResult(culture.Name)); + } + } + + return NullProviderCultureResult; + } + } +} diff --git a/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs new file mode 100644 index 0000000000..1a27798c35 --- /dev/null +++ b/src/Umbraco.Web.Common/Localization/UmbracoRequestLocalizationOptions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; + +namespace Umbraco.Web.Common.Localization +{ + /// + /// Custom Umbraco options configuration for + /// + public class UmbracoRequestLocalizationOptions : IConfigureOptions + { + private readonly IOptions _globalSettings; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRequestLocalizationOptions(IOptions globalSettings) => _globalSettings = globalSettings; + + /// + public void Configure(RequestLocalizationOptions options) + { + // set the default culture to what is in config + options.DefaultRequestCulture = new RequestCulture(_globalSettings.Value.DefaultUILanguage); + + // add a custom provider + if (options.RequestCultureProviders == null) + { + options.RequestCultureProviders = new List(); + } + + options.RequestCultureProviders.Insert(0, new UmbracoBackOfficeIdentityCultureProvider()); + options.RequestCultureProviders.Insert(1, new UmbracoPublishedContentCultureProvider()); + } + } +} diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoMvcConfigureOptions.cs b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs similarity index 95% rename from src/Umbraco.Web.Common/AspNetCore/UmbracoMvcConfigureOptions.cs rename to src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs index 6c8420cd03..c212334560 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoMvcConfigureOptions.cs +++ b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs @@ -3,8 +3,9 @@ using Microsoft.Extensions.Options; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.ModelBinders; -namespace Umbraco.Web.Common.AspNetCore +namespace Umbraco.Web.Common.Mvc { + /// /// Options for globally configuring MVC for Umbraco /// diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index f45371707a..9831652fbf 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -37,10 +37,8 @@ namespace Umbraco.Web private readonly IPublishedRouter _publishedRouter; private readonly IUmbracoContextFactory _umbracoContextFactory; private readonly RoutableDocumentFilter _routableDocumentLookup; - private readonly IRequestCache _requestCache; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private readonly UriUtility _uriUtility; public UmbracoInjectedModule( IRuntimeState runtime, @@ -48,8 +46,6 @@ namespace Umbraco.Web IPublishedRouter publishedRouter, IUmbracoContextFactory umbracoContextFactory, RoutableDocumentFilter routableDocumentLookup, - UriUtility uriUtility, - IRequestCache requestCache, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) { @@ -58,8 +54,6 @@ namespace Umbraco.Web _publishedRouter = publishedRouter; _umbracoContextFactory = umbracoContextFactory; _routableDocumentLookup = routableDocumentLookup; - _uriUtility = uriUtility; - _requestCache = requestCache; _globalSettings = globalSettings; _hostingEnvironment = hostingEnvironment; }