Forget-Me-Not/helpers/validation.nim

227 lines
11 KiB
Nim
Raw Permalink Normal View History

2025-05-17 09:02:52 -05:00
#[Copyright 2025 ITwrx.
This file is part of Forget-Me-Not.
Forget-Me-Not is released under the GNU Affero General Public License 3.0.
2025-05-15 08:01:35 -05:00
See COPYING or <https://www.gnu.org/licenses/> for details.]#
import std/[strutils, re, typetraits, times, tables]
import valido/[email, password]
import "form", "global"
var msgString{.threadvar.}: string
proc vSize(sizeName: string, sizeValue: int, inputName: string, inputData: string, inputType: string) =
case sizeName:
of "min":
#how we determine size depends on input type.
case inputType:
of "string":
if not (inputData.len >= sizeValue):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a character count of at least " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
of "integer":
if not (parseIntIf(inputData) >= sizeValue):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a value of at least " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
of "float":
if not (parseFloat(inputData) >= sizeValue.float):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a value of at least " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
of "max":
#how we determine size depends on input type.
case inputType:
of "string":
if inputData.len > sizeValue:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a character count that is no greater than " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
of "integer":
if parseIntIf(inputData) > sizeValue:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a value that is no greater than " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
of "float":
if parseFloat(inputData) > sizeValue.float:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must have a value that is no greater than " & $sizeValue & " ."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
proc vType(inputName: string, inputData: string, validators: seq[string]): string =
var inputType: string
if "integer" in validators:
try:
#don't try to parse empty string as int.
if inputData.len > 0:
discard parseIntIf(inputData)
inputType = "integer"
except:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must be a whole number."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
elif "float" in validators:
try:
discard parseFloat(inputData)
inputType = "float"
except:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must be a floating point number; a number with a decimal point. i.e. '1.5'"
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
elif "boolean" in validators:
if (inputData == "true") or not (inputData == "false"):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must be interpretable as a boolean. i.e. true, or false."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
inputType = "boolean"
else:
inputType = "string"
return inputType
proc vRegex(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
#check for regex validators.
for v in validators:
if match(v, re"^(min):([0-9]+)$"):
#get size's value from the string.
let vNameSeq = v.split(':')
let sizeName = vNameSeq[0]
let sizeValue = parseIntIf(vNameSeq[1])
vSize(sizeName, sizeValue, inputName, inputData, inputType)
if match(v, re"^(max):([0-9]+)$"):
let vNameSeq = v.split(':')
let sizeName = vNameSeq[0]
let sizeValue = parseIntIf(vNameSeq[1])
vSize(sizeName, sizeValue, inputName, inputData, inputType)
#ex. matches: "required_without:parent_id"
if match(v, re"^(required_without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData{.threadvar.}: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len == 0:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required when " & conditionField & " isn't set."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(must_unset_with):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len > 0:
if inputData.len > 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field must be empty if " & conditionField & " is not."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField = vNameSeq[1]
var conditionFieldInputData: string
conditionFieldInputData = formInput(conditionField)
if conditionFieldInputData.len > 0:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is also required when " & conditionField & " is set."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
var conditionField2InputData: string
conditionField2InputData = formInput(conditionField2)
if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required when " & conditionField1 & " is set and " & conditionField2 & " isn't set."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+):(and_without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
var conditionField2InputData: string
conditionField2InputData = formInput(conditionField2)
let conditionField3 = vNameSeq[5]
var conditionField3InputData: string
conditionField3InputData = formInput(conditionField3)
if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0 and not conditionField3InputData.len > 0:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required when " & conditionField1 & " is set and " & conditionField2 & " and " & conditionField3 & " aren't set."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required when " & conditionField1 & " equals " & conditionField2
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+):(without):([a-z_]+)$"):
let vNameSeq = v.split(':')
#skip 0 index, as that's just the name of the validator.
let conditionField1 = vNameSeq[1]
var conditionField1InputData: string
conditionField1InputData = formInput(conditionField1)
let conditionField2 = vNameSeq[3]
let conditionField3 = vNameSeq[5]
var conditionField3InputData: string
conditionField3InputData = formInput(conditionField3)
if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2 and conditionField3InputData.len == 0:
if inputData.len == 0:
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required when " & conditionField1 & " equals " & conditionField2 & " and " & conditionField3 & " is not set."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
proc vStandard(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
if "email" in validators:
if not isEmail(inputData):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is not recognized as a valid email address."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
if "min_complexity" in validators:
if not isStrongPassword(inputData):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is not random/complex enough. Try to make your " & inputName & " more unpredictable by adding random numbers, random letter casing, unrelated words, special characters, etc."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
proc vHardcoded*(inputDataAll: Table[string, string], validators: seq[string]) =
##Warning: this validator requires specific (hardcoded) input names to exist in the posted form data.
if "future_datetime" in validators:
let nowDT = now()
var inputDT: DateTime
let inputDateString = inputDataAll.getOrDefault("send_date", "")
let inputTimeHrString = inputDataAll.getOrDefault("send_time_hr", "")
let inputTimeMinString = inputDataAll.getOrDefault("send_time_min", "")
let inputTimeAmPmString = inputDataAll.getOrDefault("send_time_am_pm", "")
if inputDateString.len > 0 and inputTimeHrString.len > 0 and inputTimeMinString.len > 0 and inputTimeAmPmString.len > 0:
let inputDatetimeString = inputDateString & " " & inputTimeHrString & ":" & inputTimeMinString & ":" & inputTimeAmPmString
inputDT = parse(inputDatetimeString, "yyyy-M-d h:m:tt")
else:
inputDT = now()
if inputDT <= nowDT:
2025-05-15 08:58:20 -05:00
msgString = "The 'send_date' and 'send_time' fields combined must be a DateTime in the future (compared to the DateTime at form submit)."
2025-05-15 08:01:35 -05:00
addFormError("send_date", msgString)
proc vInput*(inputName: string, validators: seq[string]) =
var inputType: string
var inputData: string
var inputDataAll: Table[string, string]
inputData = formInput(inputName)
inputDataAll = formInputAll()
#check if 'required' is set and validate input if it exists. otherwise return error.
if "required" in validators:
if (inputData.len == 0):
2025-05-15 08:58:20 -05:00
msgString = "The '" & inputName & "' field is required."
2025-05-15 08:01:35 -05:00
addFormError(inputName, msgString)
else:
#proceed with validation.
#get inputType.
inputType = vType(inputName, inputData, validators)
#check for other validators and run if they exist.
vStandard(inputName, inputData, inputType, validators)
vHardcoded(inputDataAll, validators)
vRegex(inputName, inputData, inputType, validators)
#required is not set, but input could still exist. validate it.
else:
#get inputType.
inputType = vType(inputName, inputData, validators)
#check for other validators and run if they exist.
vStandard(inputName, inputData, inputType, validators)
vHardcoded(inputDataAll, validators)
vRegex(inputName, inputData, inputType, validators)