Make sure the form is properly validated

This commit is contained in:
nicolas.dorier
2022-11-25 16:11:13 +09:00
parent 4f65eb4d65
commit 5ff1a59a99
6 changed files with 76 additions and 48 deletions

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -19,16 +20,22 @@ public class Field
// If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form. // If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form.
public string Value; public string Value;
// The field is considered "valid" if there are no validation errors public bool Required;
public List<string> ValidationErrors = new List<string>();
public virtual bool IsValid()
{
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
}
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; } [JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new(); public List<Field> Fields { get; set; } = new();
public virtual void Validate(ModelStateDictionary modelState)
{
if (Required && string.IsNullOrEmpty(Value))
{
modelState.AddModelError(Name, "This field is required");
}
}
public bool IsValid()
{
ModelStateDictionary modelState = new ModelStateDictionary();
Validate(modelState);
return modelState.IsValid;
}
} }

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Form; namespace BTCPayServer.Abstractions.Form;
@@ -28,7 +29,7 @@ public class Form
// Are all the fields valid in the form? // Are all the fields valid in the form?
public bool IsValid() public bool IsValid()
{ {
return Fields.All(field => field.IsValid()); return Validate(null);
} }
public Field GetFieldByName(string name) public Field GetFieldByName(string name)
@@ -63,6 +64,17 @@ public class Form
} }
return null; return null;
} }
#nullable enable
public bool Validate(ModelStateDictionary? modelState)
{
modelState ??= new ModelStateDictionary();
foreach (var field in Fields)
field.Validate(modelState);
return modelState.IsValid;
}
#nullable restore
public List<string> GetAllNames() public List<string> GetAllNames()
{ {
return GetAllNames(Fields); return GetAllNames(Fields);

View File

@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Abstractions.Form; namespace BTCPayServer.Abstractions.Form;
public class HtmlInputField : Field public class HtmlInputField : Field
@@ -11,7 +13,6 @@ public class HtmlInputField : Field
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user. // A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText; public string HelpText;
public bool Required;
public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text") public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text")
{ {
Label = label; Label = label;
@@ -22,6 +23,5 @@ public class HtmlInputField : Field
HelpText = helpText; HelpText = helpText;
Type = type; Type = type;
} }
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types. // TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
} }

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -181,7 +182,7 @@ namespace BTCPayServer.Controllers
[HttpGet("{payReqId}/form")] [HttpGet("{payReqId}/form")]
[HttpPost("{payReqId}/form")] [HttpPost("{payReqId}/form")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, [FromForm] string formId, [FromForm] string formData) public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
{ {
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId()); var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (result == null) if (result == null)
@@ -195,32 +196,37 @@ namespace BTCPayServer.Controllers
{ {
case null: case null:
case { } when string.IsNullOrEmpty(prFormId): case { } when string.IsNullOrEmpty(prFormId):
break; case { } when Request.Method == "GET" && prBlob.FormResponse is not null:
return RedirectToAction("ViewPaymentRequest", new { payReqId });
case { } when Request.Method == "GET" && prBlob.FormResponse is null:
break;
default: default:
// POST case: Handle form submit // POST case: Handle form submit
if (!string.IsNullOrEmpty(formData) && formId == prFormId) var formData = Form.Parse(Forms.UIFormsController.GetFormData(prFormId).Config);
formData.ApplyValuesFromForm(this.Request.Form);
if (formData.IsValid())
{ {
prBlob.FormResponse = JObject.Parse(formData); prBlob.FormResponse = JObject.FromObject(formData.GetValues());
result.SetBlob(prBlob); result.SetBlob(prBlob);
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result); await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
return RedirectToAction("PayPaymentRequest", new { payReqId }); return RedirectToAction("PayPaymentRequest", new { payReqId });
} }
break;
// GET or empty form data case: Redirect to form }
return View("PostRedirect", new PostRedirectViewModel
{ return View("PostRedirect", new PostRedirectViewModel
AspController = "UIForms", {
AspAction = "ViewPublicForm", AspController = "UIForms",
FormParameters = AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", prFormId }
},
FormParameters =
{ {
{ "formId", prFormId },
{ "redirectUrl", Request.GetCurrentUrl() } { "redirectUrl", Request.GetCurrentUrl() }
} }
}); });
}
return RedirectToAction("ViewPaymentRequest", new { payReqId });
} }
[HttpGet("{payReqId}/pay")] [HttpGet("{payReqId}/pay")]

View File

@@ -45,38 +45,36 @@ public class UIFormsController : Controller
[AllowAnonymous] [AllowAnonymous]
[HttpPost("~/forms/{formId}")] [HttpPost("~/forms/{formId}")]
public IActionResult SubmitForm( public IActionResult SubmitForm(
string formId, string? redirectUrl, string formId,
string? redirectUrl,
[FromServices] StoreRepository storeRepository, [FromServices] StoreRepository storeRepository,
[FromServices] UIInvoiceController invoiceController) [FromServices] UIInvoiceController invoiceController)
{ {
var formData = GetFormData(formId); var formData = GetFormData(formId);
if (formData?.Config is null) if (formData?.Config is null)
{
return NotFound(); return NotFound();
} var conf = Form.Parse(formData.Config);
conf.ApplyValuesFromForm(Request.Form);
if (!conf.Validate(ModelState))
return View("View", new FormViewModel() { FormData = formData, RedirectUrl = redirectUrl });
var dbForm = Form.Parse(formData.Config); var form = new MultiValueDictionary<string, string>();
dbForm.ApplyValuesFromForm(Request.Form); foreach (var kv in Request.Form)
Dictionary<string, object> data = dbForm.GetValues(); form.Add(kv.Key, kv.Value);
// With redirect, the form comes from another entity that we need to send the data back to // With redirect, the form comes from another entity that we need to send the data back to
if (!string.IsNullOrEmpty(redirectUrl)) if (!string.IsNullOrEmpty(redirectUrl))
{ {
return View("PostRedirect", new PostRedirectViewModel return View("PostRedirect", new PostRedirectViewModel
{ {
FormUrl = redirectUrl, FormUrl = redirectUrl,
FormParameters = FormParameters = form
{
{ "formId", formId },
{ "formData", JsonConvert.SerializeObject(data) }
}
}); });
} }
return NotFound(); return NotFound();
} }
private FormData? GetFormData(string id) internal static FormData? GetFormData(string id)
{ {
FormData? form = id switch FormData? form = id switch
{ {

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
@@ -118,8 +119,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
string notificationUrl, string notificationUrl,
string redirectUrl, string redirectUrl,
string choiceKey, string choiceKey,
string formId = null,
string formData = null,
string posData = null, string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore, RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -230,9 +229,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
default: default:
// POST case: Handle form submit // POST case: Handle form submit
if (!string.IsNullOrEmpty(formData) && formId == posFormId) var formData = Form.Parse(Forms.UIFormsController.GetFormData(posFormId).Config);
formData.ApplyValuesFromForm(this.Request.Form);
if (formData.IsValid())
{ {
formResponse = JObject.Parse(formData); formResponse = JObject.FromObject(formData.GetValues());
break; break;
} }
@@ -247,9 +249,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{ {
AspController = "UIForms", AspController = "UIForms",
AspAction = "ViewPublicForm", AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", posFormId }
},
FormParameters = FormParameters =
{ {
{ "formId", posFormId },
{ "redirectUrl", Request.GetCurrentUrl() + query } { "redirectUrl", Request.GetCurrentUrl() + query }
} }
}); });