diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs similarity index 97% rename from src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs index 99a3ea4fe5..0ad8e2c421 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -12,7 +11,7 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.Models; -namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { [TestFixture] public class ContentModelBinderTests @@ -92,9 +91,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); if (withUmbracoDataToken) - { routeData.DataTokens.Add(Constants.Web.UmbracoDataToken, source); - } var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); var metadataProvider = new EmptyModelMetadataProvider(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs new file mode 100644 index 0000000000..6dd3b024b3 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using NUnit.Framework; +using Umbraco.Web.Common.ModelBinders; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders +{ + [TestFixture] + public class HttpQueryStringModelBinderTests + { + [Test] + public void Binds_Query_To_FormCollection() + { + // Arrange + var bindingContext = CreateBindingContext("?foo=bar&baz=buzz"); + var binder = new HttpQueryStringModelBinder(); + + // Act + binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + + var typedModel = bindingContext.Result.Model as FormCollection; + Assert.IsNotNull(typedModel); + Assert.AreEqual(typedModel["foo"], "bar"); + Assert.AreEqual(typedModel["baz"], "buzz"); + } + + [Test] + public void Sets_Culture_Form_Value_From_Query_If_Provided() + { + // Arrange + var bindingContext = CreateBindingContext("?foo=bar&baz=buzz&culture=en-gb"); + var binder = new HttpQueryStringModelBinder(); + + // Act + binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + + var typedModel = bindingContext.Result.Model as FormCollection; + Assert.IsNotNull(typedModel); + Assert.AreEqual(typedModel["culture"], "en-gb"); + } + + [Test] + public void Sets_Culture_Form_Value_From_Header_If_Not_Provided_In_Query() + { + // Arrange + var bindingContext = CreateBindingContext("?foo=bar&baz=buzz"); + var binder = new HttpQueryStringModelBinder(); + + // Act + binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + + var typedModel = bindingContext.Result.Model as FormCollection; + Assert.IsNotNull(typedModel); + Assert.AreEqual(typedModel["culture"], "en-gb"); + } + + private ModelBindingContext CreateBindingContext(string querystring) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString(querystring); + httpContext.Request.Headers.Add("X-UMB-CULTURE", new StringValues("en-gb")); + var routeData = new RouteData(); + var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); + var metadataProvider = new EmptyModelMetadataProvider(); + var routeValueDictionary = new RouteValueDictionary(); + var valueProvider = new RouteValueProvider(BindingSource.Path, routeValueDictionary); + var modelType = typeof(FormCollection); + return new DefaultModelBindingContext + { + ActionContext = actionContext, + ModelMetadata = metadataProvider.GetMetadataForType(modelType), + ModelName = modelType.Name, + ValueProvider = valueProvider, + }; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs new file mode 100644 index 0000000000..9dab445992 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Extensions +{ + public static class HttpRequestExtensions + { + internal static string ClientCulture(this HttpRequest request) + { + return request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null; + } + } +} diff --git a/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs new file mode 100644 index 0000000000..eb6a1ab7fb --- /dev/null +++ b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Primitives; +using Umbraco.Core; +using Umbraco.Web.Common.Extensions; + +namespace Umbraco.Web.Common.ModelBinders +{ + /// + /// Allows an Action to execute with an arbitrary number of QueryStrings + /// + /// + /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number + /// but this will allow you to do it. + /// + public sealed class HttpQueryStringModelBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var queryStrings = GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query); + var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray(); + if (queryStringKeys.InvariantContains("culture") == false) + { + queryStrings.Add("culture", new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture())); + } + + var formData = new FormCollection(queryStrings); + bindingContext.Result = ModelBindingResult.Success(formData); + return Task.CompletedTask; + } + + private Dictionary GetQueryAsDictionary(IQueryCollection query) + { + var result = new Dictionary(); + if (query == null) + { + return result; + } + + foreach (var item in query) + { + result.Add(item.Key, item.Value); + } + + return result; + } + } +} diff --git a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs index 4d58e2b512..3b8e4bea3f 100644 --- a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs +++ b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.WebApi.Filters /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number /// but this will allow you to do it /// + /// Migrated to .NET core public sealed class HttpQueryStringModelBinder : IModelBinder { public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)