#[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 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 """""" & fFieldMsg(fr, fieldName) & "
" #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'"