Apologies in advance for the code dump. I'm pretty new to both Yesod and Haskell and I'm struggling with CSRF issues. The problem as I understand it is that the form's _token isn't matching the environment token (running runFormPostNoToken works just fine). I have a pair of routes:
/ HomeR GET
/upload UploadR POST
The Handler for HomeR is defined as such:
getHomeR :: Handler Html
getHomeR = do
((res, uploadWidget), enctype) <- runFormPost imgForm
setTitle "Title"
$(widgetFile "homepage")
And the form itself and the upload Handler are:
imgForm :: Html -> MForm (HandlerT App IO) (FormResult Img, Widget)
imgForm hiddenInput = do
(titleRes, titleView) <- mreq textField uploadFormTitleSettings Nothing
(descRes, descView) <- mopt textareaField uploadFormDescriptionSettings Nothing
(fileRes, fileView) <- mreq fileField uploadFormAttachmentSettings Nothing
let imgRes = Img
<$> titleRes
<*> descRes
<*> fileRes
<*> pure (Likes 0)
<*> pure (Dislikes 0)
<*> pure (UserID 1)
<*> pure (Community 1)
let imgUploadWidget = do
toWidget
[whamlet|
^{ fvInput titleView }
^{ fvInput descView }
^{ fvInput fileView }
#{ hiddenInput }
<button type="submit">Submit
|]
return (imgRes, imgUploadWidget)
postImgUploadR :: HandlerT App IO Html
postImgUploadR = do
((imgRes, imgUploadWidget), enctype) <- runFormPost imgForm
let submission :: HandlerT App IO Html
submission = case imgRes of
FormSuccess upload -> defaultLayout [whamlet|The form was uploaded|]
FormMissing -> defaultLayout [whamlet|The form is missing|]
FormFailure upload -> defaultLayout [whamlet|The form failed.|]
submission
Unfortunately I'm not even sure what question to be asking here -- hopefully there's something obviously wrong with my code and someone can point me in the right direction. I spent some time reading through the source code of the functions and I think I understand it, but I'm not sure where the second, erroneous CSRF token is coming from (I assumed it would be set in a session variable and therefore wouldn't change). It's been quite a few hours and all my attempts to figure this out have failed.
Well, this turns out to be one of the most time-consuming bugs with one of the most mundane answers I've ever dealt with.
Some time ago I added in sslOnlySessions to makeSessionBackend and forgot about it. After trying to wrap my brain around every possible way I could have done something wrong, I took a step back, tried to think of a different angle to approach the problem from, and it hit me like a ton of bricks.
I'm posting this answer on the off chance someone 10 years from now will make the same dumb mistake I did, and after scouring their code for mistakes, finally fire up Google and find an easy answer.
Godspeed, future Haskeller.
Related
Since IO can not be used inside Yesod Template, how can I display a simple current time on a page?
In my .hamlet file, something like:
<h2>
#{show $ getCurrentTime }
getCurrentTime :: IO UTCTime
In other words, you need to run the IO action outside of the template.
That outside means the template's handler. So I would write like this.
-- Home.hs
getHomeR = do
time <- liftIO getCurrentTime
defaultLayout $(widgetFile "homepage")
-- homepage.hamlet
<h2>#{show time}
I'm trying to build a page with multiple similar forms on one page. Each form is very simple, it provides an integer input and a submit button. Each form corresponds to a counter, and the counter is supposed to be increased when the form is submitted.
incrementCounterForm :: Entity Counter -> Form (CounterId, Int)
incrementCounterForm (Entity i _) = renderBoostrap3 BootstrapInlineForm
$ (,)
<$> pure i
<*> areq intField "value" Nothing
In my GET handler I do
counters <- runDB $ selectList [] [] -- Get all the current counters
forms <- mapM (generateFormPost . incrementCounterForm) counters -- Generate the forms
Then in my hamlet file I iterate over the forms and render them all individually (they all go to the same handler).
My question relates to the POST handler. How do I do the runFormPost?
((result,_),_) <- runFormPost $ incrementCounterForm undefined
What should undefined be here? I want to get the counter from the form, not have to provide one.
EDIT: I lied about providing an arbitrary counter working
If I do provide an arbitrary Entity Counter it seems to work (the provided counter is not used in the result). Yet, I can't leave it as undefined because runFormPost seems to evaluate it.
So, I'd probably advise moving your counter ID into the URL, so you're doing something like POSTing to /counters/1/increment or something. Feels slightly off to have the ID in a hidden field.
However, if you did want to keep it in a hidden field, you can have the form take a Maybe (Entity Counter) as an argument. What you'll do is when the user GETs the page and you're generating the form, you'll pass in a (Just entity) as the argument which you'll use to populate the hidden field. When the user POSTs to you and you run the form, you'll provide no default value (because you want the value that was stored in the hidden field).
Here's an example of what that would look like:
data MyForm = MyForm
{ increment :: Integer
, counterId :: CounterId
}
deriving Show
myForm :: Maybe (Entity Counter) -> AForm Handler MyForm
myForm maybeEntity = MyForm
<$> areq intField "How much to increment?" Nothing
<*> areq hiddenField "" (entityKey <$> maybeEntity)
When generating the form, provide a value for the hidden field:
(widget, enctype) <- generateFormPost $ renderBootstrap (myForm (Just someEntity))
When running the form, don't provide a default value; the hidden field should have the data already:
((res, widget), enctype) <- runFormPost $ renderBootstrap (myForm Nothing)
I just initialized a Yesod project (no database) using yesod init.
My HomeR GET handler looks like this:
getHomeR :: Handler Html
getHomeR = do
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
When using yesod devel, I can access the default homepage at http://localhost:3000/.
How can I modify the handler listed above to retrieve (and display) a HTTP GET query parameter like id=abc123 when accessing this URL:
http://localhost:3000/?id=abc123
Note: This question was answered Q&A-style and therefore intentionally doesn't show research effort!
I'll show two different methods to achieve this. For both, you'll need to add this code to your template, e.g. in homepage.hamlet:
Note that it is not guaranteed there is any id parameter present when accessing the URL, therefore the type resulting from both methods is Maybe Text. See the Shakespearean template docs for a detailed explanation of the template parameters.
Method 1: lookupGetParam
The easiest way you can do this is using lookupGetParam like this:
idValueMaybe <- lookupGetParam "id"
When using the default setting as generated by yesod init, idValueMaybe needs to be defined in both getHomeR and postHomeR if idValueMaybe is used in the template.
Your HomeR GET handler could look like this:
getHomeR :: Handler Html
getHomeR = do
idValueMaybe <- lookupGetParam "id"
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
Method 2: reqGetParams
Instead of looking up the query parameters by name, you can also retrieve a list of query key/value pairs using reqGetParams. This can be advantageous in certain situations, e.g. if you don't know all possible keys in advance. Using the standard lookup function you can easily lookup a certain key in that list.
The relevant part of your code could look like this:
getParameters <- reqGetParams <$> getRequest
let idValueMaybe = lookup "id" getParameters :: Maybe Text
Your getHomeR could look like this:
getHomeR :: Handler Html
getHomeR = do
getParameters <- reqGetParams <$> getRequest
let idValueMaybe = lookup "id" getParameters :: Maybe Text
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
I want to share some data across requests in Yesod. In my case that data is a MVar (Data.Map Text ReadWriteLock), but I don't think the format of the data being shared matters too much here.
In Foundation.hs, there is a comment that says I can add fields to App, and every handler will have access to the data there. This seems like an approach I could use to share data between different handlers. I have been looking through the Yesod book, but I could not find any examples of getting data from App.
How would I access the newly created field from within a handler?
I think this might be a good use case for STM. I could share a TVar (Data.Map Text ReadWriteLock). But creating a TVar wraps the TVar in the STM monad. I might be mistaken, but to me that seems like the entire Yesod "main loop" would need to be run in the STM monad.
Is using STM a viable option here? Could anyone elaborate on how this might be achieved?
This tutorial for building a file server with Yesod shows quite nicely how you can use STM to access shared data. The relevant part starts from part 2.
To elaborate on pxqr's comment, you want to do something like this.
In your Foundation.hs file (assuming you started your project with yesod init).
data App = App
{ ... other fields
, shared :: TVar Int -- New shared TVar field
}
Then in your Application.hs file where you create the App instance.
makeFoundation conf = do
.... snip .....
tv <- newTVarIO 0 -- Initialize your TVar
let logger = Yesod.Core.Types.Logger loggerSet' getter
foundation = App conf s manager logger tv -- Add TVar here
return foundation
Then in your Handler use the TVar
getHomeR :: Handler Html
getHomeR = do
app <- getYesod -- Get the instance of App
x <- liftIO $ atomically $ do -- Read and update the TVar value.
val <- readTVar (shared app)
writeTVar (shared app) (val + 1)
return val
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
-- Use the TVar value (increments on each page refresh).
setTitle $ fromString (show x)
$(widgetFile "homepage")
I have searched in internet, in the Yesod Web ebook and other tutorials (Yesod Tutorial) but I have not been able to clarify this problem. I am using the scaffolded site.
I have a handler, inside it returns a value, the email if the user is authenticated or a string if he is not. What I want is to return the localized message instead the string "(Unknown User ID)". My problem is to use a value from the message file (ex. MsgHello), if I do this, it returns errors like:
Couldn't match expected type AppMessage' with actual typeText'
I have tried using (show MsgHello) or (pack MsgHello), even calling msg <- getMessageRender but I have not been able to do what I expect. If you have any suggestions, they are welcome.
Thanks!!
PD: This is part of the code that I am working on, line :
getUserProfileR :: Handler RepHtml
getUserProfileR = do
maid <- maybeAuth
let user = case maid of
Nothing -> "(Unknown User ID)"
Just (Entity _ u) -> userEmail u
defaultLayout $ do
setTitleI MsgUserProfile
$(widgetFile "nhUserProfile")
Thanks to Tickhon Jelvis for pointing out those web pages, also I found this one: Poly Hamlet i18n where I was able to get the solution to the problem.
So, if I would like to use a localized message, I would do:
getUserProfileR :: Handler RepHtml
getUserProfileR = do
maid <- maybeAuth
msg <- getRenderMessage
let user = case maid of
Nothing -> msg MsgNoUser --"(Unknown User ID)"
Just (Entity _ u) -> userEmail u
defaultLayout $ do
setTitleI MsgUserProfile
$(widgetFile "nhUserProfile")
Also remember that there is a helper function "setTitleI" which takes directly a Msg value and avoids the use of "msg MsgThisPageTitle"
My understanding of the I18N module is that you want to take your AppMessage value and use renderMessage on it.
You need to pass in a type specifying your translation type and a list of languages as well as your message. The translation type is created using the mkMessage function and the list of languages looks something like ["en-US", "en-GB", "fr"].