~alcinnz/bureaucromancy

ref: b48eccb144e10f6f81f361afaf06507037730eb3 bureaucromancy/src/Text/HTML/Form/Validate.hs -rw-r--r-- 5.5 KiB
b48eccb1 — Adrian Cochrane Integrate & fix error messages; TODO: Block invalid submits 1 year, 1 month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
{-# LANGUAGE OverloadedStrings #-}
module Text.HTML.Form.Validate(isInputValid, isInputValid', isFormValid, isFormValid',
        inputErrorMessage, inputErrorMessage', normalizeInput, normalizeForm) where

import Text.HTML.Form
import qualified Data.Text as Txt
import Text.Read (readMaybe)
import Data.Hourglass
import Network.URI (parseAbsoluteURI)
import Data.Maybe (isJust, isNothing)
import Text.Regex.TDFA ((=~), matchTest)

isFormValid :: Form -> Bool
isFormValid = all isInputValid . inputs

isFormValid' :: Form -> Bool
isFormValid' = all isInputValid' . inputs

isInputValid :: Input -> Bool
isInputValid = null . inputErrorMessage

isInputValid' :: Input -> Bool
isInputValid' = null . inputErrorMessage'

inputErrorMessage :: Input -> [Char]
inputErrorMessage Input { inputType = "hidden" } = "" -- Don't validate hiddens!
inputErrorMessage self@Input { required = True }
    | inputType self == "checkbox", not $ checked self = "Required!"
    -- Not validating "radio", needs different API...
    | value self == "" = "Required!"
inputErrorMessage Input { value = "" } = "" -- Skip further checks for empty!
inputErrorMessage self@Input { pattern = Just re }
    | not $ re `matchTest` value self = "Invalid format!"
inputErrorMessage Input { lengthRange = (Just min', _), value = val }
    | Txt.length val < min' = "Must be at least " ++ show min' ++ " characters!"
inputErrorMessage Input { lengthRange = (_, Just max'), value = val }
    | Txt.length val > max' = "Must be at most " ++ show max' ++ " characters!"
inputErrorMessage Input { range = (Just min', _), value = val }
    | Just x <- readMaybe' val :: Maybe Float, Just y <- readMaybe' min', x < y =
        "Must be at least " ++ Txt.unpack min' ++ "!"
inputErrorMessage Input { range = (_, Just max'), value = val }
    | Just x <- readMaybe' val :: Maybe Float, Just y <- readMaybe' max', x > y =
        "Must be at most " ++ Txt.unpack max' ++ "!"
inputErrorMessage Input { range = (Just min', _), step = Just step', value = val }
    | Just x <- readMaybe' val :: Maybe Integer, Just y <- readMaybe' min',
        Just z <- readMaybe' step', z /= 0, rem (x - y) z == 0 =
            ("Must be in increments of " ++ Txt.unpack step' ++ " from "
            ++ Txt.unpack min' ++ "!")
inputErrorMessage Input { range = (Just min', _), value = val }
    | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack min',
        x < y = "Must be at least " ++ Txt.unpack min' ++ "!"
inputErrorMessage Input { range = (_, Just max'), value = val }
    | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack max',
        x > y = "Must be at most " ++ Txt.unpack max' ++ "!"
inputErrorMessage Input {
        range = (Just min', _), step = Just step',
        inputType = ty, value = val
      }
    | ty == "date", Just x <- parseTime $ Txt.unpack val,
        Just y <- parseTime $ Txt.unpack min', Just z <- readMaybe' step',
        timeDiff x y `rem` toSeconds mempty { durationSeconds = 24*z } == Seconds 0 =
            ("Must be in increments of " ++ Txt.unpack step' ++ " days from " ++
            Txt.unpack min' ++ "!")
    | ty == "month" = "" -- Not prepared to properly validate this...
    | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack min',
        Just z <- readMaybe' step', timeDiff x y `rem` Seconds z == Seconds 0 =
            ("Must be in increments of " ++ Txt.unpack step' ++ "s from " ++
            Txt.unpack min' ++ "!")

-- Validation specific to input types
inputErrorMessage self@Input { inputType = "color" }
    | ("#[0-9a-fA-F]{6}" :: String) =~ value self = "Invalid colour value!"
inputErrorMessage self@Input { inputType = "date" } = isTime' self
inputErrorMessage self@Input { inputType = "datetime" } = isTime' self
inputErrorMessage self@Input { inputType = "datetime-local" } = isTime' self
-- This validation is less strict than many sites expect, but don't over-validate...
inputErrorMessage self@Input { inputType = "email" }
    | '@' `Txt.elem` value self =
        "Obviously invalid email address, needs an '@'!"
inputErrorMessage self@Input { inputType = "month" } = isTime' self
inputErrorMessage Input { inputType = "number", value = val }
    | isNothing (readMaybe' val :: Maybe Float) = "Invalid number!"
inputErrorMessage Input { inputType = "range", value = val }
    | isNothing (readMaybe' val :: Maybe Float) = "Invalid number!"
inputErrorMessage self@Input { inputType = "time" } = isTime' self
inputErrorMessage self@Input { inputType = "url" }
    | isURL $ value self = "Invalid web address!"
inputErrorMessage self@Input { inputType = "week" } = isTime' self
inputErrorMessage _ = ""

inputErrorMessage' :: Input -> [Char]
inputErrorMessage' = inputErrorMessage . normalizeInput

parseTime :: String -> Maybe DateTime
parseTime = fmap localTimeUnwrap . localTimeParse ISO8601_DateAndTime
isTime :: Input -> Bool
isTime = isJust . parseTime . Txt.unpack . value
isTime' :: Input -> String
isTime' x | isTime x = ""
    | otherwise = "Invalid time format!"
readMaybe' :: Read a => Txt.Text -> Maybe a
readMaybe' = readMaybe . Txt.unpack
isURL :: Txt.Text -> Bool
isURL = isNothing . parseAbsoluteURI . Txt.unpack

normalizeInput :: Input -> Input
normalizeInput self@Input { inputType = "url", value = val }
    | not $ ':' `Txt.elem` val = self { -- Is there a better check?
            value = "https://" `Txt.append` val
        }
-- Other aspects we wish to normalize?
normalizeInput self = self

normalizeForm :: Form -> Form
normalizeForm self = self { inputs = map normalizeInput $ inputs self }