From 7497865d1f5169bbd15a18d4481e1b94c38c101c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 9 Mar 2019 23:40:22 +0900 Subject: [PATCH] Pay button was not working properly if the server was not en-US (Fix #638) --- .../InvariantDecimalModelBinder.cs | 99 +++++++++++++++++++ .../StoreViewModels/PayButtonViewModel.cs | 3 + 2 files changed, 102 insertions(+) create mode 100644 BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs diff --git a/BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs b/BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs new file mode 100644 index 000000000..1a95de163 --- /dev/null +++ b/BTCPayServer/ModelBinders/InvariantDecimalModelBinder.cs @@ -0,0 +1,99 @@ +// Copied and adjusted from https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs +using System; +using System.Globalization; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace BTCPayServer.ModelBinders +{ + /// + /// An for and where T is + /// . + /// + public class InvariantDecimalModelBinder : IModelBinder + { + private readonly NumberStyles _supportedStyles; + + public InvariantDecimalModelBinder() + { + _supportedStyles = NumberStyles.Any; + } + + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + + var modelName = bindingContext.ModelName; + var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + if (valueProviderResult == ValueProviderResult.None) + { + return Task.CompletedTask; + } + + var modelState = bindingContext.ModelState; + modelState.SetModelValue(modelName, valueProviderResult); + + var metadata = bindingContext.ModelMetadata; + var type = metadata.UnderlyingOrModelType; + try + { + var value = valueProviderResult.FirstValue; + var culture = CultureInfo.InvariantCulture; + + object model; + if (string.IsNullOrWhiteSpace(value)) + { + // Parse() method trims the value (with common NumberStyles) then throws if the result is empty. + model = null; + } + else if (type == typeof(decimal)) + { + model = decimal.Parse(value, _supportedStyles, culture); + } + else + { + // unreachable + throw new NotSupportedException(); + } + + // When converting value, a null model may indicate a failed conversion for an otherwise required + // model (can't set a ValueType to null). This detects if a null model value is acceptable given the + // current bindingContext. If not, an error is logged. + if (model == null && !metadata.IsReferenceOrNullableType) + { + modelState.TryAddModelError( + modelName, + metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( + valueProviderResult.ToString())); + } + else + { + bindingContext.Result = ModelBindingResult.Success(model); + } + } + catch (Exception exception) + { + var isFormatException = exception is FormatException; + if (!isFormatException && exception.InnerException != null) + { + // Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve + // this code in case a cursory review of the CoreFx code missed something. + exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException; + } + + modelState.TryAddModelError(modelName, exception, metadata); + + // Conversion failed. + } + + return Task.CompletedTask; + } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs index ccb388e39..2c6687e93 100644 --- a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.ModelBinders; +using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Models.StoreViewModels { public class PayButtonViewModel { + [ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))] public decimal Price { get; set; } public string InvoiceId { get; set; } [Required]