Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep incorrect user input after POST/Redirect/GET using yesod-form

Recently I stumbled across the following problem:
Using yesod I wanted to

  1. Display an applicative form and let the user POST his data to a special Handler
  2. On FormFailure redirect the browser to the previous page
  3. Print the error messages ...
  4. ... and redisplay the form already filled with the incorrect user-supplied data

As far as I'm concerned this is what POST/Redirect/GET is all about.

While the points <1-3> required a simple and straightforward implementation, I found it impossible to achieve point <4>!
The yesod-form package automatically handles this issue, but doesn't allow any redirects between form parsing <2> and error handling <3,4> as I would like it to be.

Response to Michael Snoyman's Answer

You suggest I should serialize the submitted data and somehow inject it into the form after the redirect. This leads to more detailed questions:

  1. How to get the data to serialize?

    I know I could use runRequestBody:: GHandler s m RequestBodyContents, but which are the relevant information (The names of the fields are generated automatically)?

  2. How to inject the data into the form?

    If you look at the type of e.g. aopt

    aopt :: Field sub master a -> FieldSettings master -> Maybe (Maybe a) -> AForm sub master (Maybe a)
    

    you'll see that it requires the default value to be of the same type as the Field, so it's not possible to re-insert user supplied data which might not parse correctly.

    Example: The user types an 'A' into an intField. Now I want to be able to display the 'A' in the same field after a redirect, but the API doesn't allow me to.

How should I deal with this problem?

like image 406
Jakub Avatar asked Mar 28 '13 13:03

Jakub


2 Answers

I personally think it's acceptable to return a filled-in form with a POST request, which is what the yesod-form API is optimized for. If you want to force a redirect on form submission failures as well, you'll need to serialize the submitted data and store it somewhere, e.g.:

  1. In the database.
  2. In the user session.
  3. As part of the query string parameters for the URL you redirect to. Note that this approach is not suitable for sensitive data, as any intermediate proxies would cache the form data.
like image 133
Michael Snoyman Avatar answered Nov 12 '22 23:11

Michael Snoyman


Old question, but I needed this today, so might as well post it for others running into the same issue.

Basically, as Michael suggests, we can serialize the data to the session. Doing this is tricky, plus getting it into a form is even trickier. I had to rip postEnv and postHelper from Yesod.Form.Functions since they are not exported but are needed to do this.

You can then use setLastInvalidPost in your handler before a redirect then use generateFormFromLastPost in the destination handler.

Note that it would probably be better to use something like Data.Serialize for serialization; however, Show/Read instances were good enough for my needs (and much simpler).

Here's the good stuff. If you want a full working snippet, you can check out my gist.

-- Create a form from last post data in the session if exists, otherwise create a blank form.
generateFormFromLastPost :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m) =>
                            (Markup -> MForm m (FormResult a, xml)) -> m (xml, Enctype)
generateFormFromLastPost form = do
    env <- getLastInvalidPost
    case env of
        Nothing -> generateFormPost form
        Just _ -> first snd <$> postHelper form env

lastInvalidPostSessionKey :: Text
lastInvalidPostSessionKey = "lastInvalidPost"

-- Sets the post data retreived from postEnv, ignoring the FileEnv.
setLastInvalidPost :: MonadHandler m => Maybe (Env, FileEnv) -> m ()
setLastInvalidPost Nothing = return ()
setLastInvalidPost (Just (env, _)) = sessionSetter lastInvalidPostSessionKey env

-- Retrieves the previous post data to be passed to postHelper.
getLastInvalidPost :: MonadHandler m => m (Maybe (Env, FileEnv))
getLastInvalidPost = do
    result <- sessionGetter lastInvalidPostSessionKey
    return $ case result of
        Nothing -> Nothing
        Just env -> Just (env, Map.fromList [])

sessionSetter :: (MonadHandler m, Show a) => Text -> a -> m ()
sessionSetter key = setSession key . pack . show

sessionGetter :: (MonadHandler m, Read b) => Text -> m (Maybe b)
sessionGetter key = do
    m <- lookupSession key
    return $ readMaybe . unpack =<< m
like image 40
pyrospade Avatar answered Nov 12 '22 21:11

pyrospade