Forget-Me-Not/helpers/form.nim

214 lines
7.4 KiB
Nim

#[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.
See COPYING or <https://www.gnu.org/licenses/> for details.]#
#TODO: remove anything not being used by Forget-Me-Not.
import std/[cgi, strtabs, strutils, cookies, uri, tables]
import jsony, guildenstern/httpserver, sqliteral
import "auth", "global"
type
FormError* = object
fieldName*, fieldMessage*: string
type
FormOldInput* = object
fieldName*, fieldOldInput*: string
type
FormResult* = object
id*: int
message*, messageClass*, errors*, oldInputs*: string
var formError*: FormError
var formErrors*: seq[FormError]
var formOldInput*: FormOldInput
var formOldInputs*: seq[FormOldInput]
var formResult*: FormResult
#might still be in use by cookie FR type. too lazy to investigate now.
proc clearFormResult*() =
formResult = FormResult(message : "",
messageClass : "",
errors : "",
oldInputs : "")
proc assignErrorFR*(formErrors: seq[FormError], formOldInputs: seq[FormOldInput]): FormResult =
{.gcsafe.}:
formResult.message = "Error(s) encountered while validating form input(s)."
formResult.messageClass = "form-error"
formResult.errors = toJson(formErrors)
formResult.oldInputs = toJson(formOldInputs)
proc assignGeneralErrorFR*(errorMsg: string): FormResult =
{.gcsafe.}:
formResult.message = errorMsg
formResult.messageClass = "form-error"
proc assignGeneralSuccessFR*(successMsg: string): FormResult =
{.gcsafe.}:
formResult.message = successMsg
formResult.messageClass = "form-success"
proc assignLoginSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "You have logged in successfully."
formResult.messageClass = "form-success"
proc assignCECreateSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully created."
formResult.messageClass = "form-success"
proc assignCEEditSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully edited."
formResult.messageClass = "form-success"
proc assignCEDeleteSuccessFR*(): FormResult =
{.gcsafe.}:
formResult.message = "Content Entity successfully deleted."
formResult.messageClass = "form-success"
#using cookie to store login page formResult, because there's no session to use as formResult id yet.
proc getCookieFormResult*(): FormResult =
let cookieString = http.headers.getOrDefault("cookie")
let cookiesTable = parseCookies(cookieString)
var frJsonString: string
if cookiesTable.hasKey("form_result"):
frJsonString = cookiesTable["form_result"]
var formResult: FormResult
if frJsonString.len > 0:
formResult = frJsonString.fromJson(FormResult)
formErrors = @[]
clearFormResult()
if not formResult.message.len > 0:
formResult.id = 0
formResult.message = ""
formResult.messageClass = ""
formResult.errors = ""
formResult.oldInputs = ""
return formResult
#using strtabs for storing all other formResults.
proc getFormResult*(): FormResult =
let sessionId = getSessionIdFromCookies()
var frJsonString, strTabSessionId: string
if frStrTab.hasKey("sessionId"):
strTabSessionId = frStrTab["sessionId"]
if strTabSessionId == sessionId:
if frStrTab.hasKey("frJson"):
frJsonString = frStrTab["frJson"]
var newFormResult: FormResult
if frJsonString.len > 0:
newFormResult = frJsonString.fromJson(FormResult)
#reset formErrors seq variable used in form handlers.
formErrors = @[]
#clear the existing formResult so it won't show on next page's GET (without new POST).
clear(frStrTab, modeCaseSensitive)
if not newFormResult.message.len > 0:
newFormResult.id = 0
newFormResult.message = ""
newFormResult.messageClass = ""
newFormResult.errors = ""
newFormResult.oldInputs = ""
return newFormResult
proc setFR*() =
let sessionId = getSessionIdFromCookies()
let frJson = formResult.toJson()
frStrTab = {"sessionId": sessionId, "frJson": frJson}.newStringTable
proc formInput*(input: string): string =
{.gcsafe.}:
if server.contenttype == Compact:
#readData:cgi, getBody:Guildenstern, getOrDefault:strtabs.
return readData(getBody()).getOrDefault(input)
else:
#getMPStringInput(input: string)
echo "not url-encoded"
proc formInputAll*(): Table[string, string] =
{.gcsafe.}:
if server.contenttype == Compact:
#getBody:Guildenstern
let formDataStr = getBody()
var formData = initTable[string, string]()
# Use decodeQuery from the uri module
for (key, value) in decodeQuery(formDataStr):
formData[key] = value
return formData
else:
echo "not url-encoded"
proc formInputInt*(input: string): int =
{.gcsafe.}:
let readInput = readData(getBody()).getOrDefault(input)
return parseIntIf(readInput)
template formInputSeq*(input: string): seq[string] =
{.gcsafe.}:
readData(getBody()).getOrDefault(input)
proc addFormError*(inputName: string, msgString: string) =
{.gcsafe.}:
formError.fieldName = inputName
formError.fieldMessage = msgString
formErrors.add(formError)
proc addFormOldInput*(inputName: string, inputData: string) =
{.gcsafe.}:
formOldInput.fieldName = inputName
formOldInput.fieldOldInput = inputData
formOldInputs.add(formOldInput)
#takes a fieldName and returns the fieldMessage.
proc fFieldMsg*(fr: FormResult, fieldName: string): string =
{.gcsafe.}:
if fr.errors.len() > 0:
let errors = fromJson(fr.errors, seq[FormError])
for error in errors:
if error.fieldName == fieldName:
return error.fieldMessage
#this may need to handle more than one message per form field at some point, but not for this app (yet).
proc fErrorMsg*(fr: FormResult, fieldName:string): string =
{.gcsafe.}:
if fFieldMsg(fr, fieldName).len > 0:
return """<span class="text-red-500">""" & fFieldMsg(fr, fieldName) & "</span><br>"
#takes a fieldName and returns the fieldOldInput.
proc fOldInput*(fr: FormResult, fieldName: string, defaultValue = ""): string =
{.gcsafe.}:
var fieldOldInput: string
if fr.oldInputs.len() > 0:
let oldInputs = fromJson(fr.oldInputs, seq[FormOldInput])
for oldInput in oldInputs:
if oldInput.fieldName == fieldName:
fieldOldInput = oldInput.fieldOldInput
return fieldOldInput
else:
return defaultValue
proc getOldInputJson*(): string =
{.gcsafe.}:
let postData = readData(getBody())
#pairs is a strtabs iterator.
for key,value in pairs(postData):
if key.len() > 0:
if key != "password" and key != "csrf_token":
addFormOldInput(key, value)
return toJson(formOldInputs)
#the empty string defaultValue makes it possible to optionally supply a value from the DB: as needed in edit forms.
#checked proc works on radios and checboxes.
proc checked*(fr: FormResult, groupName: string, targetValue: string, defaultValue = ""): string =
{.gcsafe.}:
if fOldInput(fr, groupName, defaultValue) == targetValue:
return "checked"
proc selected*(fr: FormResult, selectName: string, targetValue: string, defaultValue = ""): string =
{.gcsafe.}:
if fOldInput(fr, selectName, defaultValue) == targetValue:
return "selected='selected'"