diff --git a/BTCPayServer.Tests/NewBlocks.bat b/BTCPayServer.Tests/NewBlocks.bat new file mode 100644 index 00000000000..25eee5d0b6f --- /dev/null +++ b/BTCPayServer.Tests/NewBlocks.bat @@ -0,0 +1,2 @@ +PowerShell.exe -command ".\docker-bitcoin-generate.ps1 3" +pause diff --git a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs index 0889979f2b2..78117c43c9a 100644 --- a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs +++ b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs @@ -5,11 +5,15 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Form; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Filters; +using BTCPayServer.Forms; +using BTCPayServer.Forms.Models; +using BTCPayServer.Models; using BTCPayServer.Plugins.Crowdfund.Models; using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Services.Apps; @@ -20,7 +24,10 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.DataEncoders; using NBitpayClient; +using Newtonsoft.Json.Linq; using NicolasDorier.RateLimits; using CrowdfundResetEvery = BTCPayServer.Services.Apps.CrowdfundResetEvery; @@ -37,6 +44,7 @@ public UICrowdfundController( StoreRepository storeRepository, UIInvoiceController invoiceController, UserManager userManager, + FormDataService formDataService, CrowdfundAppType app) { _currencies = currencies; @@ -46,6 +54,7 @@ public UICrowdfundController( _storeRepository = storeRepository; _eventAggregator = eventAggregator; _invoiceController = invoiceController; + FormDataService = formDataService; } private readonly EventAggregator _eventAggregator; @@ -55,6 +64,7 @@ public UICrowdfundController( private readonly UIInvoiceController _invoiceController; private readonly UserManager _userManager; private readonly CrowdfundAppType _app; + public FormDataService FormDataService { get; } [HttpGet("/")] [HttpGet("/apps/{appId}/crowdfund")] @@ -95,7 +105,7 @@ public async Task ViewCrowdfund(string appId) [EnableCors(CorsPolicies.All)] [DomainMappingConstraint(CrowdfundAppType.AppType)] [RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)] - public async Task ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken) + public async Task ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken = default, string formResponse = null) { var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true); @@ -121,6 +131,26 @@ public async Task ContributeToCrowdfund(string appId, ContributeT return NotFound("Crowdfund is not currently active"); } + JObject formResponseJObject = null; + + if (settings.FormId is not null) + { + var formData = await FormDataService.GetForm(settings.FormId); + if (formData is not null) + { + formResponseJObject = TryParseJObject(formResponse) ?? new JObject(); + var form = Form.Parse(formData.Config); + FormDataService.SetValues(form, formResponseJObject); + if (!FormDataService.Validate(form, ModelState)) + { + //someone tried to bypass validation + return RedirectToAction(nameof(ViewCrowdfund), new { appId }); + } + + } + + } + var store = await _appService.GetStore(app); var title = settings.Title; decimal? price = request.Amount; @@ -203,6 +233,11 @@ public async Task ContributeToCrowdfund(string appId, ContributeT entity.FullNotifications = true; entity.ExtendedNotifications = true; entity.Metadata.OrderUrl = appUrl; + if (formResponseJObject is null) + return; + var meta = entity.Metadata.ToJObject(); + meta.Merge(formResponseJObject); + entity.Metadata = InvoiceMetadata.FromJObject(meta); }); if (request.RedirectToCheckout) @@ -219,6 +254,102 @@ public async Task ContributeToCrowdfund(string appId, ContributeT } } + private JObject TryParseJObject(string posData) + { + try + { + return JObject.Parse(posData); + } + catch + { + } + return null; + } + + + [HttpGet("/apps/{appId}/crowdfund/form")] + [IgnoreAntiforgeryToken] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + public async Task CrowdfundForm(string appId) + { + var app = await _appService.GetApp(appId, CrowdfundAppType.AppType); + if (app == null) + return NotFound(); + + var settings = app.GetSettings(); + var formData = await FormDataService.GetForm(settings.FormId); + if (formData is null) + { + return RedirectToAction(nameof(ViewCrowdfund), new { appId }); + } + + var prefix = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)) + "_"; + var formParameters = new MultiValueDictionary(); + var controller = nameof(UICrowdfundController).TrimEnd("Controller", StringComparison.InvariantCulture); + var store = await _appService.GetStore(app); + var storeBlob = store.GetStoreBlob(); + var form = Form.Parse(formData.Config); + form.ApplyValuesFromForm(Request.Query); + var vm = new FormViewModel + { + StoreName = store.StoreName, + StoreBranding = new StoreBrandingViewModel(storeBlob), + FormName = formData.Name, + Form = form, + AspController = controller, + AspAction = nameof(CrowdfundFormSubmit), + RouteParameters = new Dictionary { { "appId", appId } }, + FormParameters = formParameters, + FormParameterPrefix = prefix + }; + + return View("Views/UIForms/View", vm); + } + + [HttpPost("/apps/{appId}/crowdfund/form/submit")] + [IgnoreAntiforgeryToken] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + public async Task CrowdfundFormSubmit(string appId, FormViewModel viewModel) + { + var app = await _appService.GetApp(appId, CrowdfundAppType.AppType); + if (app == null) + return NotFound(); + + var settings = app.GetSettings(); + var formData = await FormDataService.GetForm(settings.FormId); + if (formData is null) + { + return RedirectToAction(nameof(ViewCrowdfund)); + } + var form = Form.Parse(formData.Config); + var formFieldNames = form.GetAllFields().Select(tuple => tuple.FullName).Distinct().ToArray(); + var formParameters = Request.Form + .Where(pair => pair.Key.StartsWith(viewModel.FormParameterPrefix)) + .ToDictionary(pair => pair.Key.Replace(viewModel.FormParameterPrefix, string.Empty), pair => pair.Value) + .ToMultiValueDictionary(p => p.Key, p => p.Value.ToString()); + + form.ApplyValuesFromForm(Request.Form.Where(pair => formFieldNames.Contains(pair.Key))); + + if (FormDataService.Validate(form, ModelState)) + { + var appInfo = await GetAppInfo(appId); + var req = new ContributeToCrowdfund() + { + RedirectToCheckout = true, + ViewCrowdfundViewModel = appInfo + }; + + return ContributeToCrowdfund(appId, req, formResponse: FormDataService.GetValues(form).ToString()).Result; + } + + viewModel.FormName = formData.Name; + viewModel.Form = form; + + viewModel.FormParameters = formParameters; + return View("Views/UIForms/View", viewModel); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [HttpGet("{appId}/settings/crowdfund")] public async Task UpdateCrowdfund(string appId) @@ -264,7 +395,8 @@ public async Task UpdateCrowdfund(string appId) DisplayPerksValue = settings.DisplayPerksValue, SortPerksByPopularity = settings.SortPerksByPopularity, Sounds = string.Join(Environment.NewLine, settings.Sounds), - AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors) + AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors), + FormId = settings.FormId }; return View("Crowdfund/UpdateCrowdfund", vm); } @@ -373,7 +505,8 @@ public async Task UpdateCrowdfund(string appId, UpdateCrowdfundVi DisplayPerksRanking = vm.DisplayPerksRanking, SortPerksByPopularity = vm.SortPerksByPopularity, Sounds = parsedSounds, - AnimationColors = parsedAnimationColors + AnimationColors = parsedAnimationColors, + FormId = vm.FormId }; app.TagAllInvoices = vm.UseAllStoreInvoices; diff --git a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs index ec8b57b249b..cea44832817 100644 --- a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs +++ b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs @@ -11,7 +11,6 @@ using BTCPayServer.Models; using BTCPayServer.Plugins.Crowdfund.Controllers; using BTCPayServer.Plugins.Crowdfund.Models; -using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -210,6 +209,7 @@ public Task> GetItemStats(AppData appData, InvoiceEntity[ PerkCount = perkCount, PerkValue = perkValue, NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never, + HasFormForExtraValues = (settings.FormId is not null), Sounds = settings.Sounds, AnimationColors = settings.AnimationColors, CurrencyData = _currencyNameTable.GetCurrencyData(settings.TargetCurrency, true), diff --git a/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs b/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs index 66d4d80e878..db2a2d7a756 100644 --- a/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs +++ b/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs @@ -117,6 +117,10 @@ public class UpdateCrowdfundViewModel // NOTE: Improve validation if needed public bool ModelWithMinimumData => Description != null && Title != null && TargetCurrency != null; + + [Display(Name = "Request contributor data on checkout")] + public string FormId { get; set; } + public bool Archived { get; set; } } } diff --git a/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs b/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs index 58bf15b7494..f8f51a59c80 100644 --- a/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs +++ b/BTCPayServer/Plugins/Crowdfund/Models/ViewCrowdfundViewModel.cs @@ -36,6 +36,7 @@ public class ViewCrowdfundViewModel public string[] Sounds { get; set; } public int ResetEveryAmount { get; set; } public bool NeverReset { get; set; } + public bool HasFormForExtraValues { get; set; } public Dictionary PerkCount { get; set; } diff --git a/BTCPayServer/Services/Apps/CrowdfundSettings.cs b/BTCPayServer/Services/Apps/CrowdfundSettings.cs index 2b303c0bae3..e70bfe5c7bc 100644 --- a/BTCPayServer/Services/Apps/CrowdfundSettings.cs +++ b/BTCPayServer/Services/Apps/CrowdfundSettings.cs @@ -45,6 +45,9 @@ public decimal? TargetAmount public bool DisplayPerksRanking { get; set; } public bool DisplayPerksValue { get; set; } public bool SortPerksByPopularity { get; set; } + public string FormId { get; set; } = null; + + public string[] AnimationColors { get; set; } = { "#FF6138", "#FFBE53", "#2980B9", "#282741" diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml index 2fa2eaf8150..717d7dd50a2 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml @@ -207,7 +207,13 @@
- + @if (Model.HasFormForExtraValues) + { + + } + else { + + }
diff --git a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml index b591c2b2fd0..33703405f4a 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml @@ -4,11 +4,14 @@ @using BTCPayServer.TagHelpers @using BTCPayServer.Views.Apps @using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Forms +@inject FormDataService FormDataService @inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel @{ ViewData.SetActivePage(AppsNavPages.Update, "Update Crowdfund", Model.AppId); Csp.UnsafeEval(); + var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId); } @section PageHeadContent { @@ -174,6 +177,11 @@

Contributions

+
+ + + +