{-# LANGUAGE QuasiQuotes #-} {-# 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 -- * Rendering , FormRender , renderTable , renderDivs , renderDivsNoLabels , renderBootstrap -- * Validation , check , checkBool , checkM , checkMMap , customErrorMessage -- * Utilities , fieldSettingsLabel , parseHelper ) 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) import Control.Monad.Trans.Class import Control.Monad (liftM, join) import Crypto.Classes (constTimeEq) import Text.Blaze (Markup, toMarkup) #define Html Markup #define toHtml toMarkup import Yesod.Core 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 1) -- | 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 = "_token" let token = case reqToken req of Nothing -> mempty Just n -> [shamlet||] m <- getYesod langs <- languages ((res, xml), enctype) <- runFormGeneric (form token) m langs env let res' = case (res, env) of (FormSuccess{}, Just (params, _)) | not (Map.lookup tokenKey params === reqToken req) -> FormFailure [renderMessage m langs MsgCsrfWarning] _ -> res where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constTimeEq` TE.encodeUtf8 t2 Nothing === Nothing = True -- It's important to use constTimeEq _ === _ = False -- in order to avoid timing attacks. 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 (FormResult a, xml)) -> m ((FormResult a, xml), 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 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||] langs <- languages m <- getYesod runFormGeneric (form fragment) m langs env type FormRender m a = AForm m a -> Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()) renderTable, renderDivs, renderDivsNoLabels :: Monad m => FormRender m a renderTable aform fragment = do (res, views') <- aFormToForm aform let views = views' [] let widget = [whamlet| $newline never \#{fragment} $forall view <- views