123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- {-# LANGUAGE QuasiQuotes #-}
- {-# LANGUAGE TupleSections #-}
- {-# LANGUAGE TypeFamilies #-}
- {-# LANGUAGE OverloadedStrings #-}
- {-# LANGUAGE RecordWildCards #-}
- {-# LANGUAGE FlexibleContexts #-}
- {-# LANGUAGE CPP #-}
- module Yesod.Form.Functions
- ( -- * Running in MForm monad
- newFormIdent
- , askParams
- , askFiles
- -- * Applicative/Monadic conversion
- , formToAForm
- , aFormToForm
- -- * Fields to Forms
- , mreq
- , mopt
- , areq
- , aopt
- -- * Run a form
- , runFormPost
- , runFormPostNoToken
- , runFormGet
- -- * Generate a blank form
- , generateFormPost
- , generateFormGet'
- , generateFormGet
- -- * More than one form on a handler
- , identifyForm
- -- * Rendering
- , FormRender
- , renderTable
- , renderDivs
- , renderDivsNoLabels
- , renderBootstrap
- , renderBootstrap2
- -- * Validation
- , check
- , checkBool
- , checkM
- , checkMMap
- , customErrorMessage
- -- * Utilities
- , fieldSettingsLabel
- , parseHelper
- , parseHelperGen
- , convertField
- ) where
- import Yesod.Form.Types
- import Data.Text (Text, pack)
- import Control.Arrow (second)
- import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local)
- import Control.Monad.Trans.Class
- import Control.Monad (liftM, join)
- import Data.Byteable (constEqBytes)
- import Text.Blaze (Markup, toMarkup)
- #define Html Markup
- #define toHtml toMarkup
- import Yesod.Core
- import Yesod.Core.Handler (defaultCsrfParamName)
- import Network.Wai (requestMethod)
- import Text.Hamlet (shamlet)
- import Data.Monoid (mempty)
- import Data.Maybe (listToMaybe, fromMaybe)
- import qualified Data.Map as Map
- import qualified Data.Text.Encoding as TE
- import Control.Arrow (first)
- -- | Get a unique identifier.
- newFormIdent :: Monad m => MForm m Text
- newFormIdent = do
- i <- get
- let i' = incrInts i
- put i'
- return $ pack $ 'f' : show i'
- where
- incrInts (IntSingle i) = IntSingle $ i + 1
- incrInts (IntCons i is) = (i + 1) `IntCons` is
- formToAForm :: (HandlerSite m ~ site, Monad m)
- => MForm m (FormResult a, [FieldView site])
- -> AForm m a
- formToAForm form = AForm $ \(site, langs) env ints -> do
- ((a, xmls), ints', enc) <- runRWST form (env, site, langs) ints
- return (a, (++) xmls, ints', enc)
- aFormToForm :: (Monad m, HandlerSite m ~ site)
- => AForm m a
- -> MForm m (FormResult a, [FieldView site] -> [FieldView site])
- aFormToForm (AForm aform) = do
- ints <- get
- (env, site, langs) <- ask
- (a, xml, ints', enc) <- lift $ aform (site, langs) env ints
- put ints'
- tell enc
- return (a, xml)
- askParams :: Monad m => MForm m (Maybe Env)
- askParams = do
- (x, _, _) <- ask
- return $ liftM fst x
- askFiles :: Monad m => MForm m (Maybe FileEnv)
- askFiles = do
- (x, _, _) <- ask
- return $ liftM snd x
- -- | Converts a form field into monadic form. This field requires a value
- -- and will return 'FormFailure' if left empty.
- mreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
- => Field m a -- ^ form field
- -> FieldSettings site -- ^ settings for this field
- -> Maybe a -- ^ optional default value
- -> MForm m (FormResult a, FieldView site)
- mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
- -- | Converts a form field into monadic form. This field is optional, i.e.
- -- if filled in, it returns 'Just a', if left empty, it returns 'Nothing'.
- -- Arguments are the same as for 'mreq' (apart from type of default value).
- mopt :: (site ~ HandlerSite m, MonadHandler m)
- => Field m a
- -> FieldSettings site
- -> Maybe (Maybe a)
- -> MForm m (FormResult (Maybe a), FieldView site)
- mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
- mhelper :: (site ~ HandlerSite m, MonadHandler m)
- => Field m a
- -> FieldSettings site
- -> Maybe a
- -> (site -> [Text] -> FormResult b) -- ^ on missing
- -> (a -> FormResult b) -- ^ on success
- -> Bool -- ^ is it required?
- -> MForm m (FormResult b, FieldView site)
- mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
- tell fieldEnctype
- mp <- askParams
- name <- maybe newFormIdent return fsName
- theId <- lift $ maybe newIdent return fsId
- (_, site, langs) <- ask
- let mr2 = renderMessage site langs
- (res, val) <-
- case mp of
- Nothing -> return (FormMissing, maybe (Left "") Right mdef)
- Just p -> do
- mfs <- askFiles
- let mvals = fromMaybe [] $ Map.lookup name p
- files = fromMaybe [] $ mfs >>= Map.lookup name
- emx <- lift $ fieldParse mvals files
- return $ case emx of
- Left (SomeMessage e) -> (FormFailure [renderMessage site langs e], maybe (Left "") Left (listToMaybe mvals))
- Right mx ->
- case mx of
- Nothing -> (onMissing site langs, Left "")
- Just x -> (onFound x, Right x)
- return (res, FieldView
- { fvLabel = toHtml $ mr2 fsLabel
- , fvTooltip = fmap toHtml $ fmap mr2 fsTooltip
- , fvId = theId
- , fvInput = fieldView theId name fsAttrs val isReq
- , fvErrors =
- case res of
- FormFailure [e] -> Just $ toHtml e
- _ -> Nothing
- , fvRequired = isReq
- })
- -- | Applicative equivalent of 'mreq'.
- areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
- => Field m a
- -> FieldSettings site
- -> Maybe a
- -> AForm m a
- areq a b = formToAForm . liftM (second return) . mreq a b
- -- | Applicative equivalent of 'mopt'.
- aopt :: MonadHandler m
- => Field m a
- -> FieldSettings (HandlerSite m)
- -> Maybe (Maybe a)
- -> AForm m (Maybe a)
- aopt a b = formToAForm . liftM (second return) . mopt a b
- runFormGeneric :: Monad m
- => MForm m a
- -> HandlerSite m
- -> [Text]
- -> Maybe (Env, FileEnv)
- -> m (a, Enctype)
- runFormGeneric form site langs env = evalRWST form (env, site, langs) (IntSingle 0)
- -- | This function is used to both initially render a form and to later extract
- -- results from it. Note that, due to CSRF protection and a few other issues,
- -- forms submitted via GET and POST are slightly different. As such, be sure to
- -- call the relevant function based on how the form will be submitted, /not/
- -- the current request method.
- --
- -- For example, a common case is displaying a form on a GET request and having
- -- the form submit to a POST page. In such a case, both the GET and POST
- -- handlers should use 'runFormPost'.
- runFormPost :: (RenderMessage (HandlerSite m) FormMessage, MonadResource m, MonadHandler m)
- => (Html -> MForm m (FormResult a, xml))
- -> m ((FormResult a, xml), Enctype)
- runFormPost form = do
- env <- postEnv
- postHelper form env
- postHelper :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage)
- => (Html -> MForm m (FormResult a, xml))
- -> Maybe (Env, FileEnv)
- -> m ((FormResult a, xml), Enctype)
- postHelper form env = do
- req <- getRequest
- let tokenKey = defaultCsrfParamName
- let token =
- case reqToken req of
- Nothing -> mempty
- Just n -> [shamlet|<input type=hidden name=#{tokenKey} value=#{n}>|]
- m <- getYesod
- langs <- languages
- ((res, xml), enctype) <- runFormGeneric (form token) m langs env
- let res' =
- case (res, env) of
- (_, Nothing) -> FormMissing
- (FormSuccess{}, Just (params, _))
- | not (Map.lookup tokenKey params === reqToken req) ->
- FormFailure [renderMessage m langs MsgCsrfWarning]
- _ -> res
- -- It's important to use constant-time comparison (constEqBytes) in order to avoid timing attacks.
- where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constEqBytes` TE.encodeUtf8 t2
- Nothing === Nothing = True
- _ === _ = False
- return ((res', xml), enctype)
- -- | Similar to 'runFormPost', except it always ignores the currently available
- -- environment. This is necessary in cases like a wizard UI, where a single
- -- page will both receive and incoming form and produce a new, blank form. For
- -- general usage, you can stick with @runFormPost@.
- generateFormPost
- :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)
- => (Html -> MForm m (FormResult a, xml))
- -> m (xml, Enctype)
- generateFormPost form = first snd `liftM` postHelper form Nothing
- postEnv :: (MonadHandler m, MonadResource m)
- => m (Maybe (Env, FileEnv))
- postEnv = do
- req <- getRequest
- if requestMethod (reqWaiRequest req) == "GET"
- then return Nothing
- else do
- (p, f) <- runRequestBody
- let p' = Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) p
- return $ Just (p', Map.unionsWith (++) $ map (\(k, v) -> Map.singleton k [v]) f)
- runFormPostNoToken :: MonadHandler m
- => (Html -> MForm m a)
- -> m (a, Enctype)
- runFormPostNoToken form = do
- langs <- languages
- m <- getYesod
- env <- postEnv
- runFormGeneric (form mempty) m langs env
- runFormGet :: MonadHandler m
- => (Html -> MForm m a)
- -> m (a, Enctype)
- runFormGet form = do
- gets <- liftM reqGetParams getRequest
- let env =
- case lookup getKey gets of
- Nothing -> Nothing
- Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty)
- getHelper form env
- {- FIXME: generateFormGet' "Will be renamed to generateFormGet in next version of Yesod" -}
- -- |
- --
- -- Since 1.3.11
- generateFormGet'
- :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)
- => (Html -> MForm m (FormResult a, xml))
- -> m (xml, Enctype)
- generateFormGet' form = first snd `liftM` getHelper form Nothing
- {-# DEPRECATED generateFormGet "Will require RenderMessage in next version of Yesod" #-}
- generateFormGet :: MonadHandler m
- => (Html -> MForm m a)
- -> m (a, Enctype)
- generateFormGet form = getHelper form Nothing
- getKey :: Text
- getKey = "_hasdata"
- getHelper :: MonadHandler m
- => (Html -> MForm m a)
- -> Maybe (Env, FileEnv)
- -> m (a, Enctype)
- getHelper form env = do
- let fragment = [shamlet|<input type=hidden name=#{getKey}>|]
- langs <- languages
- m <- getYesod
- runFormGeneric (form fragment) m langs env
- -- | Creates a hidden field on the form that identifies it. This
- -- identification is then used to distinguish between /missing/
- -- and /wrong/ form data when a single handler contains more than
- -- one form.
- --
- -- For instance, if you have the following code on your handler:
- --
- -- > ((fooRes, fooWidget), fooEnctype) <- runFormPost fooForm
- -- > ((barRes, barWidget), barEnctype) <- runFormPost barForm
- --
- -- Then replace it with
- --
- -- > ((fooRes, fooWidget), fooEnctype) <- runFormPost $ identifyForm "foo" fooForm
- -- > ((barRes, barWidget), barEnctype) <- runFormPost $ identifyForm "bar" barForm
- --
- -- Note that it's your responsibility to ensure that the
- -- identification strings are unique (using the same one twice on a
- -- single handler will not generate any errors). This allows you
- -- to create a variable number of forms and still have them work
- -- even if their number or order change between the HTML
- -- generation and the form submission.
- identifyForm
- :: Monad m
- => Text -- ^ Form identification string.
- -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
- -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
- identifyForm identVal form = \fragment -> do
- -- Create hidden <input>.
- let fragment' =
- [shamlet|
- <input type=hidden name=#{identifyFormKey} value=#{identVal}>
- #{fragment}
- |]
- -- Check if we got its value back.
- mp <- askParams
- let missing = (mp >>= Map.lookup identifyFormKey) /= Just [identVal]
- -- Run the form proper (with our hidden <input>). If the
- -- data is missing, then do not provide any params to the
- -- form, which will turn its result into FormMissing. Also,
- -- doing this avoids having lots of fields with red errors.
- let eraseParams | missing = local (\(_, h, l) -> (Nothing, h, l))
- | otherwise = id
- eraseParams (form fragment')
- identifyFormKey :: Text
- identifyFormKey = "_formid"
- type FormRender m a =
- AForm m a
- -> Html
- -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ())
- renderTable, renderDivs, renderDivsNoLabels :: Monad m => FormRender m a
- -- | Render a form into a series of tr tags. Note that, in order to allow
- -- you to add extra rows to the table, this function does /not/ wrap up
- -- the resulting HTML in a table tag; you must do that yourself.
- renderTable aform fragment = do
- (res, views') <- aFormToForm aform
- let views = views' []
- let widget = [whamlet|
- $newline never
- $if null views
- \#{fragment}
- $forall (isFirst, view) <- addIsFirst views
- <tr :fvRequired view:.required :not $ fvRequired view:.optional>
- <td>
- $if isFirst
- \#{fragment}
- <label for=#{fvId view}>#{fvLabel view}
- $maybe tt <- fvTooltip view
- <div .tooltip>#{tt}
- <td>^{fvInput view}
- $maybe err <- fvErrors view
- <td .errors>#{err}
- |]
- return (res, widget)
- where
- addIsFirst [] = []
- addIsFirst (x:y) = (True, x) : map (False, ) y
- -- | render a field inside a div
- renderDivs = renderDivsMaybeLabels True
- -- | render a field inside a div, not displaying any label
- renderDivsNoLabels = renderDivsMaybeLabels False
- renderDivsMaybeLabels :: Monad m => Bool -> FormRender m a
- renderDivsMaybeLabels withLabels aform fragment = do
- (res, views') <- aFormToForm aform
- let views = views' []
- let widget = [whamlet|
- $newline never
- \#{fragment}
- $forall view <- views
- <div :fvRequired view:.required :not $ fvRequired view:.optional>
- $if withLabels
- <label for=#{fvId view}>#{fvLabel view}
- $maybe tt <- fvTooltip view
- <div .tooltip>#{tt}
- ^{fvInput view}
- $maybe err <- fvErrors view
- <div .errors>#{err}
- |]
- return (res, widget)
- -- | Render a form using Bootstrap v2-friendly shamlet syntax.
- -- If you're using Bootstrap v3, then you should use the
- -- functions from module "Yesod.Form.Bootstrap3".
- --
- -- Sample Hamlet:
- --
- -- > <form .form-horizontal method=post action=@{ActionR} enctype=#{formEnctype}>
- -- > <fieldset>
- -- > <legend>_{MsgLegend}
- -- > $case result
- -- > $of FormFailure reasons
- -- > $forall reason <- reasons
- -- > <div .alert .alert-error>#{reason}
- -- > $of _
- -- > ^{formWidget}
- -- > <div .form-actions>
- -- > <input .btn .primary type=submit value=_{MsgSubmit}>
- --
- -- Since 1.3.14
- renderBootstrap2 :: Monad m => FormRender m a
- renderBootstrap2 aform fragment = do
- (res, views') <- aFormToForm aform
- let views = views' []
- has (Just _) = True
- has Nothing = False
- let widget = [whamlet|
- $newline never
- \#{fragment}
- $forall view <- views
- <div .control-group .clearfix :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.error>
- <label .control-label for=#{fvId view}>#{fvLabel view}
- <div .controls .input>
- ^{fvInput view}
- $maybe tt <- fvTooltip view
- <span .help-block>#{tt}
- $maybe err <- fvErrors view
- <span .help-block>#{err}
- |]
- return (res, widget)
- -- | Deprecated synonym for 'renderBootstrap2'.
- renderBootstrap :: Monad m => FormRender m a
- renderBootstrap = renderBootstrap2
- {-# DEPRECATED renderBootstrap "Please use the Yesod.Form.Bootstrap3 module." #-}
- check :: (Monad m, RenderMessage (HandlerSite m) msg)
- => (a -> Either msg a)
- -> Field m a
- -> Field m a
- check f = checkM $ return . f
- -- | Return the given error message if the predicate is false.
- checkBool :: (Monad m, RenderMessage (HandlerSite m) msg)
- => (a -> Bool) -> msg -> Field m a -> Field m a
- checkBool b s = check $ \x -> if b x then Right x else Left s
- checkM :: (Monad m, RenderMessage (HandlerSite m) msg)
- => (a -> m (Either msg a))
- -> Field m a
- -> Field m a
- checkM f = checkMMap f id
- -- | Same as 'checkM', but modifies the datatype.
- --
- -- In order to make this work, you must provide a function to convert back from
- -- the new datatype to the old one (the second argument to this function).
- --
- -- Since 1.1.2
- checkMMap :: (Monad m, RenderMessage (HandlerSite m) msg)
- => (a -> m (Either msg b))
- -> (b -> a)
- -> Field m a
- -> Field m b
- checkMMap f inv field = field
- { fieldParse = \ts fs -> do
- e1 <- fieldParse field ts fs
- case e1 of
- Left msg -> return $ Left msg
- Right Nothing -> return $ Right Nothing
- Right (Just a) -> liftM (either (Left . SomeMessage) (Right . Just)) $ f a
- , fieldView = \i n a eres req -> fieldView field i n a (fmap inv eres) req
- }
- -- | Allows you to overwrite the error message on parse error.
- customErrorMessage :: Monad m => SomeMessage (HandlerSite m) -> Field m a -> Field m a
- customErrorMessage msg field = field
- { fieldParse = \ts fs ->
- liftM (either (const $ Left msg) Right)
- $ fieldParse field ts fs
- }
- -- | Generate a 'FieldSettings' from the given label.
- fieldSettingsLabel :: RenderMessage site msg => msg -> FieldSettings site
- fieldSettingsLabel msg = FieldSettings (SomeMessage msg) Nothing Nothing Nothing []
- -- | A helper function for creating custom fields.
- --
- -- This is intended to help with the common case where a single input value is
- -- required, such as when parsing a text field.
- --
- -- Since 1.1
- parseHelper :: (Monad m, RenderMessage site FormMessage)
- => (Text -> Either FormMessage a)
- -> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
- parseHelper = parseHelperGen
- -- | A generalized version of 'parseHelper', allowing any type for the message
- -- indicating a bad parse.
- --
- -- Since 1.3.6
- parseHelperGen :: (Monad m, RenderMessage site msg)
- => (Text -> Either msg a)
- -> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
- parseHelperGen _ [] _ = return $ Right Nothing
- parseHelperGen _ ("":_) _ = return $ Right Nothing
- parseHelperGen f (x:_) _ = return $ either (Left . SomeMessage) (Right . Just) $ f x
- -- | Since a 'Field' cannot be a 'Functor', it is not obvious how to "reuse" a Field
- -- on a @newtype@ or otherwise equivalent type. This function allows you to convert
- -- a @Field m a@ to a @Field m b@ assuming you provide a bidireccional
- -- convertion among the two, through the first two functions.
- --
- -- A simple example:
- --
- -- > import Data.Monoid
- -- > sumField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m (Sum Int)
- -- > sumField = convertField Sum getSum intField
- --
- -- Another example, not using a newtype, but instead creating a Lazy Text field:
- --
- -- > import qualified Data.Text.Lazy as TL
- -- > TextField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m TL.Text
- -- > lazyTextField = convertField TL.fromStrict TL.toStrict textField
- --
- -- Since 1.3.16
- convertField :: (Functor m)
- => (a -> b) -> (b -> a)
- -> Field m a -> Field m b
- convertField to from (Field fParse fView fEnctype) = let
- fParse' ts = fmap (fmap (fmap to)) . fParse ts
- fView' ti tn at ei = fView ti tn at (fmap from ei)
- in Field fParse' fView' fEnctype
|