State for session

This commit is contained in:
Michael Snoyman 2010-10-26 08:38:45 +02:00
parent 05b4d3e9ce
commit 1069df2665
6 changed files with 54 additions and 66 deletions

View File

@ -71,6 +71,8 @@ import Web.Routes
import Control.Arrow (first) import Control.Arrow (first)
import System.Random (randomR, newStdGen) import System.Random (randomR, newStdGen)
import qualified Data.Map as Map
#if TEST #if TEST
import Test.Framework (testGroup, Test) import Test.Framework (testGroup, Test)
import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.Framework.Providers.QuickCheck2 (testProperty)
@ -265,10 +267,12 @@ toWaiApp' y segments env = do
let eurl' = either (const Nothing) Just eurl let eurl' = either (const Nothing) Just eurl
let eh er = runHandler (errorHandler' er) render eurl' id y id let eh er = runHandler (errorHandler' er) render eurl' id y id
let ya = runHandler h render eurl' id y id let ya = runHandler h render eurl' id y id
(s, hs, ct, c, sessionFinal) <- unYesodApp ya eh rr types let sessionMap = Map.fromList
$ filter (\(x, _) -> x /= nonceKey) session'
(s, hs, ct, c, sessionFinal) <- unYesodApp ya eh rr types sessionMap
let sessionVal = encodeSession key' exp' host let sessionVal = encodeSession key' exp' host
$ (nonceKey, reqNonce rr) $ Map.toList
: sessionFinal $ Map.insert nonceKey (reqNonce rr) sessionFinal
let hs' = AddCookie (clientSessionDuration y) sessionName let hs' = AddCookie (clientSessionDuration y) sessionName
(bsToChars sessionVal) (bsToChars sessionVal)
: hs : hs
@ -337,7 +341,7 @@ parseWaiRequest env session' = do
Nothing -> do Nothing -> do
g <- newStdGen g <- newStdGen
return $ fst $ randomString 10 g return $ fst $ randomString 10 g
return $ Request gets' cookies' session' rbthunk env langs''' nonce return $ Request gets' cookies' rbthunk env langs''' nonce
where where
randomString len = randomString len =
first (map toChar) . sequence' (replicate len (randomR (0, 61))) first (map toChar) . sequence' (replicate len (randomR (0, 61)))

View File

@ -58,6 +58,7 @@ module Yesod.Handler
, alreadyExpired , alreadyExpired
, expiresAt , expiresAt
-- * Session -- * Session
, lookupSession
, setSession , setSession
, deleteSession , deleteSession
-- ** Ultimate destination -- ** Ultimate destination
@ -82,7 +83,6 @@ module Yesod.Handler
import Prelude hiding (catch) import Prelude hiding (catch)
import Yesod.Request import Yesod.Request
import Yesod.Internal import Yesod.Internal
import Data.List (foldl')
import Data.Neither import Data.Neither
import Data.Time (UTCTime) import Data.Time (UTCTime)
@ -94,6 +94,7 @@ import Control.Monad.IO.Class
import Control.Monad.Trans.Class import Control.Monad.Trans.Class
import Control.Monad.Trans.Writer import Control.Monad.Trans.Writer
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import System.IO import System.IO
import qualified Network.Wai as W import qualified Network.Wai as W
@ -103,6 +104,7 @@ import Text.Hamlet
import Control.Monad.Invert (MonadInvertIO (..)) import Control.Monad.Invert (MonadInvertIO (..))
import Control.Monad (liftM) import Control.Monad (liftM)
import qualified Data.Map as Map
#if TEST #if TEST
import Test.Framework (testGroup, Test) import Test.Framework (testGroup, Test)
@ -162,16 +164,18 @@ type GHInner s m =
ReaderT (HandlerData s m) ( ReaderT (HandlerData s m) (
MEitherT HandlerContents ( MEitherT HandlerContents (
WriterT (Endo [Header]) ( WriterT (Endo [Header]) (
WriterT (Endo [(String, Maybe String)]) ( StateT SessionMap ( -- session
IO IO
)))) ))))
type SessionMap = Map.Map String String
instance MonadInvertIO (GHandler s m) where instance MonadInvertIO (GHandler s m) where
newtype InvertedIO (GHandler s m) a = newtype InvertedIO (GHandler s m) a =
InvGHandlerIO InvGHandlerIO
{ runInvGHandlerIO :: InvertedIO (GHInner s m) a { runInvGHandlerIO :: InvertedIO (GHInner s m) a
} }
type InvertedArg (GHandler s m) = (HandlerData s m, ()) type InvertedArg (GHandler s m) = (HandlerData s m, (SessionMap, ()))
invertIO = liftM (fmap InvGHandlerIO) . invertIO . unGHandler invertIO = liftM (fmap InvGHandlerIO) . invertIO . unGHandler
revertIO f = GHandler $ revertIO $ liftM runInvGHandlerIO . f revertIO f = GHandler $ revertIO $ liftM runInvGHandlerIO . f
@ -185,7 +189,8 @@ newtype YesodApp = YesodApp
:: (ErrorResponse -> YesodApp) :: (ErrorResponse -> YesodApp)
-> Request -> Request
-> [ContentType] -> [ContentType]
-> IO (W.Status, [Header], ContentType, Content, [(String, String)]) -> SessionMap
-> IO (W.Status, [Header], ContentType, Content, SessionMap)
} }
data HandlerContents = data HandlerContents =
@ -227,16 +232,6 @@ getCurrentRoute = handlerRoute <$> GHandler ask
getRouteToMaster :: GHandler sub master (Route sub -> Route master) getRouteToMaster :: GHandler sub master (Route sub -> Route master)
getRouteToMaster = handlerToMaster <$> GHandler ask getRouteToMaster = handlerToMaster <$> GHandler ask
modifySession :: [(String, String)] -> (String, Maybe String)
-> [(String, String)]
modifySession orig (k, v) =
case v of
Nothing -> dropKeys k orig
Just v' -> (k, v') : dropKeys k orig
dropKeys :: String -> [(String, x)] -> [(String, x)]
dropKeys k = filter $ \(x, _) -> x /= k
-- | Function used internally by Yesod in the process of converting a -- | Function used internally by Yesod in the process of converting a
-- 'GHandler' into an 'W.Application'. Should not be needed by users. -- 'GHandler' into an 'W.Application'. Should not be needed by users.
runHandler :: HasReps c runHandler :: HasReps c
@ -247,7 +242,8 @@ runHandler :: HasReps c
-> master -> master
-> (master -> sub) -> (master -> sub)
-> YesodApp -> YesodApp
runHandler handler mrender sroute tomr ma tosa = YesodApp $ \eh rr cts -> do runHandler handler mrender sroute tomr ma tosa =
YesodApp $ \eh rr cts initSession -> do
let toErrorHandler = let toErrorHandler =
InternalError InternalError
. (show :: Control.Exception.SomeException -> String) . (show :: Control.Exception.SomeException -> String)
@ -259,17 +255,16 @@ runHandler handler mrender sroute tomr ma tosa = YesodApp $ \eh rr cts -> do
, handlerRender = mrender , handlerRender = mrender
, handlerToMaster = tomr , handlerToMaster = tomr
} }
((contents', headers), session') <- E.catch ( ((contents', headers), finalSession) <- E.catch (
runWriterT flip runStateT initSession
$ runWriterT $ runWriterT
$ runMEitherT $ runMEitherT
$ flip runReaderT hd $ flip runReaderT hd
$ unGHandler handler $ unGHandler handler
) (\e -> return ((MLeft $ HCError $ toErrorHandler e, id), id)) ) (\e -> return ((MLeft $ HCError $ toErrorHandler e, id), initSession))
let contents = meither id (HCContent . chooseRep) contents' let contents = meither id (HCContent . chooseRep) contents'
let finalSession = foldl' modifySession (reqSession rr) $ session' []
let handleError e = do let handleError e = do
(_, hs, ct, c, sess) <- unYesodApp (eh e) safeEh rr cts (_, hs, ct, c, sess) <- unYesodApp (eh e) safeEh rr cts finalSession
let hs' = headers hs let hs' = headers hs
return (getStatus e, hs', ct, c, sess) return (getStatus e, hs', ct, c, sess)
let sendFile' ct fp = let sendFile' ct fp =
@ -288,9 +283,10 @@ runHandler handler mrender sroute tomr ma tosa = YesodApp $ \eh rr cts -> do
(handleError . toErrorHandler) (handleError . toErrorHandler)
safeEh :: ErrorResponse -> YesodApp safeEh :: ErrorResponse -> YesodApp
safeEh er = YesodApp $ \_ _ _ -> do safeEh er = YesodApp $ \_ _ _ session -> do
liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
return (W.status500, [], typePlain, toContent "Internal Server Error", []) return (W.status500, [], typePlain, toContent "Internal Server Error",
session)
-- | Redirect to the given route. -- | Redirect to the given route.
redirect :: RedirectType -> Route master -> GHandler sub master a redirect :: RedirectType -> Route master -> GHandler sub master a
@ -334,9 +330,9 @@ setUltDest' = do
Nothing -> return () Nothing -> return ()
Just r -> do Just r -> do
tm <- getRouteToMaster tm <- getRouteToMaster
gets <- reqGetParams <$> getRequest gets' <- reqGetParams <$> getRequest
render <- getUrlRenderParams render <- getUrlRenderParams
setUltDestString $ render (tm r) gets setUltDestString $ render (tm r) gets'
-- | Redirect to the ultimate destination in the user's session. Clear the -- | Redirect to the ultimate destination in the user's session. Clear the
-- value from the session. -- value from the session.
@ -355,9 +351,6 @@ msgKey = "_MSG"
-- | Sets a message in the user's session. -- | Sets a message in the user's session.
-- --
-- The message set here will not be visible within the current request;
-- instead, it will only appear in the next request.
--
-- See 'getMessage'. -- See 'getMessage'.
setMessage :: Html -> GHandler sub master () setMessage :: Html -> GHandler sub master ()
setMessage = setSession msgKey . lbsToChars . renderHtml setMessage = setSession msgKey . lbsToChars . renderHtml
@ -412,7 +405,8 @@ setCookie a b = addHeader . AddCookie a b
deleteCookie :: String -> GHandler sub master () deleteCookie :: String -> GHandler sub master ()
deleteCookie = addHeader . DeleteCookie deleteCookie = addHeader . DeleteCookie
-- | Set the language in the user session. Will show up in 'languages'. -- | Set the language in the user session. Will show up in 'languages' on the
-- next request.
setLanguage :: String -> GHandler sub master () setLanguage :: String -> GHandler sub master ()
setLanguage = setSession langKey setLanguage = setSession langKey
@ -448,17 +442,14 @@ expiresAt = setHeader "Expires" . formatRFC1123
-- The session is handled by the clientsession package: it sets an encrypted -- The session is handled by the clientsession package: it sets an encrypted
-- and hashed cookie on the client. This ensures that all data is secure and -- and hashed cookie on the client. This ensures that all data is secure and
-- not tampered with. -- not tampered with.
--
-- Please note that the value you set here will not be available via
-- 'getSession' until the /next/ request.
setSession :: String -- ^ key setSession :: String -- ^ key
-> String -- ^ value -> String -- ^ value
-> GHandler sub master () -> GHandler sub master ()
setSession k v = GHandler . lift . lift . lift . tell $ (:) (k, Just v) setSession k = GHandler . lift . lift . lift . modify . Map.insert k
-- | Unsets a session variable. See 'setSession'. -- | Unsets a session variable. See 'setSession'.
deleteSession :: String -> GHandler sub master () deleteSession :: String -> GHandler sub master ()
deleteSession k = GHandler . lift . lift . lift . tell $ (:) (k, Nothing) deleteSession = GHandler . lift . lift . lift . modify . Map.delete
-- | Internal use only, not to be confused with 'setHeader'. -- | Internal use only, not to be confused with 'setHeader'.
addHeader :: Header -> GHandler sub master () addHeader :: Header -> GHandler sub master ()
@ -486,6 +477,12 @@ localNoCurrent :: GHandler s m a -> GHandler s m a
localNoCurrent = localNoCurrent =
GHandler . local (\hd -> hd { handlerRoute = Nothing }) . unGHandler GHandler . local (\hd -> hd { handlerRoute = Nothing }) . unGHandler
-- | Lookup for session data.
lookupSession :: ParamName -> GHandler s m (Maybe ParamValue)
lookupSession n = GHandler $ do
m <- lift $ lift $ lift get
return $ Map.lookup n m
#if TEST #if TEST
testSuite :: Test testSuite :: Test

View File

@ -28,13 +28,11 @@ module Yesod.Request
, lookupGetParam , lookupGetParam
, lookupPostParam , lookupPostParam
, lookupCookie , lookupCookie
, lookupSession
, lookupFile , lookupFile
-- ** Multi-lookup -- ** Multi-lookup
, lookupGetParams , lookupGetParams
, lookupPostParams , lookupPostParams
, lookupCookies , lookupCookies
, lookupSessions
, lookupFiles , lookupFiles
-- * Parameter type synonyms -- * Parameter type synonyms
, ParamName , ParamName
@ -98,8 +96,6 @@ data FileInfo = FileInfo
data Request = Request data Request = Request
{ reqGetParams :: [(ParamName, ParamValue)] { reqGetParams :: [(ParamName, ParamValue)]
, reqCookies :: [(ParamName, ParamValue)] , reqCookies :: [(ParamName, ParamValue)]
-- | Session data stored in a cookie via the clientsession package.
, reqSession :: [(ParamName, ParamValue)]
-- | The POST parameters and submitted files. This is stored in an IO -- | The POST parameters and submitted files. This is stored in an IO
-- thunk, which essentially means it will be computed once at most, but -- thunk, which essentially means it will be computed once at most, but
-- only if requested. This allows avoidance of the potentially costly -- only if requested. This allows avoidance of the potentially costly
@ -163,13 +159,3 @@ lookupCookies :: RequestReader m => ParamName -> m [ParamValue]
lookupCookies pn = do lookupCookies pn = do
rr <- getRequest rr <- getRequest
return $ lookup' pn $ reqCookies rr return $ lookup' pn $ reqCookies rr
-- | Lookup for session data.
lookupSession :: RequestReader m => ParamName -> m (Maybe ParamValue)
lookupSession = liftM listToMaybe . lookupSessions
-- | Lookup for session data.
lookupSessions :: RequestReader m => ParamName -> m [ParamValue]
lookupSessions pn = do
rr <- getRequest
return $ lookup' pn $ reqSession rr

View File

@ -47,6 +47,7 @@ import Yesod.Internal
import Control.Monad.Invert (MonadInvertIO (..)) import Control.Monad.Invert (MonadInvertIO (..))
import Control.Monad (liftM) import Control.Monad (liftM)
import qualified Data.Map as Map
-- | A generic widget, allowing specification of both the subsite and master -- | A generic widget, allowing specification of both the subsite and master
-- site datatypes. This is basically a large 'WriterT' stack keeping track of -- site datatypes. This is basically a large 'WriterT' stack keeping track of
@ -73,7 +74,7 @@ instance MonadInvertIO (GWidget s m) where
{ runInvGWidgetIO :: InvertedIO (GWInner s m) a { runInvGWidgetIO :: InvertedIO (GWInner s m) a
} }
type InvertedArg (GWidget s m) = type InvertedArg (GWidget s m) =
(Int, (HandlerData s m, ())) (Int, (HandlerData s m, (Map.Map String String, ())))
invertIO = liftM (fmap InvGWidgetIO) . invertIO . unGWidget invertIO = liftM (fmap InvGWidgetIO) . invertIO . unGWidget
revertIO f = GWidget $ revertIO $ liftM runInvGWidgetIO . f revertIO f = GWidget $ revertIO $ liftM runInvGWidgetIO . f

View File

@ -37,10 +37,10 @@ wrapper h = [$hamlet|
#wrapper ^h^ #wrapper ^h^
%footer Brought to you by Yesod Widgets&trade; %footer Brought to you by Yesod Widgets&trade;
|] |]
getRootR = defaultLayout $ flip wrapWidget wrapper $ do getRootR = defaultLayout $ wrapper $ do
i <- newIdent i <- newIdent
setTitle $ string "Hello Widgets" setTitle $ string "Hello Widgets"
addStyle [$cassius| addCassius [$cassius|
#$i$ #$i$
color: red color: red
|] |]
@ -48,7 +48,7 @@ getRootR = defaultLayout $ flip wrapWidget wrapper $ do
addStylesheetRemote "http://localhost:3000/static/style2.css" addStylesheetRemote "http://localhost:3000/static/style2.css"
addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"
addScript $ StaticR $ StaticRoute ["script.js"] [] addScript $ StaticR $ StaticRoute ["script.js"] []
addBody [$hamlet| addHamlet [$hamlet|
%h1#$i$ Welcome to my first widget!!! %h1#$i$ Welcome to my first widget!!!
%p %p
%a!href=@RootR@ Recursive link. %a!href=@RootR@ Recursive link.
@ -58,10 +58,10 @@ getRootR = defaultLayout $ flip wrapWidget wrapper $ do
%a!href=@CustomFormR@ Custom form arrangement. %a!href=@CustomFormR@ Custom form arrangement.
%p.noscript Your script did not load. :( %p.noscript Your script did not load. :(
|] |]
addHead [$hamlet|%meta!keywords=haskell|] addHtmlHead [$hamlet|%meta!keywords=haskell|]
handleFormR = do handleFormR = do
(res, form, enctype, hidden) <- runFormPost $ fieldsToTable $ (,,,,,,,,,,) (res, form, enctype, hidden) <- runFormPost $ fieldsToTable $ (,,,,,,,,,)
<$> stringField (FormFieldSettings "My Field" "Some tooltip info" Nothing Nothing) Nothing <$> stringField (FormFieldSettings "My Field" "Some tooltip info" Nothing Nothing) Nothing
<*> stringField ("Another field") (Just "some default text") <*> stringField ("Another field") (Just "some default text")
<*> intField (FormFieldSettings "A number field" "some nums" Nothing Nothing) (Just 5) <*> intField (FormFieldSettings "A number field" "some nums" Nothing Nothing) (Just 5)
@ -72,7 +72,6 @@ handleFormR = do
, jdsNumberOfMonths = Right (2, 3) , jdsNumberOfMonths = Right (2, 3)
} ("A day field") Nothing } ("A day field") Nothing
<*> timeField ("A time field") Nothing <*> timeField ("A time field") Nothing
<*> jqueryDayTimeField ("A day/time field") Nothing
<*> boolField FormFieldSettings <*> boolField FormFieldSettings
{ ffsLabel = "A checkbox" { ffsLabel = "A checkbox"
, ffsTooltip = "" , ffsTooltip = ""
@ -86,23 +85,23 @@ handleFormR = do
<*> maybeEmailField ("An e-mail addres") Nothing <*> maybeEmailField ("An e-mail addres") Nothing
<*> maybeTextareaField "A text area" Nothing <*> maybeTextareaField "A text area" Nothing
let mhtml = case res of let mhtml = case res of
FormSuccess (_, _, _, _, _, _, _, _, x, _, _) -> Just x FormSuccess (_, _, _, _, _, _, _, x, _, _) -> Just x
_ -> Nothing _ -> Nothing
let txt = case res of let txt = case res of
FormSuccess (_, _, _, _, _, _, _, _, _, _, Just x) -> Just x FormSuccess (_, _, _, _, _, _, _, _, _, Just x) -> Just x
_ -> Nothing _ -> Nothing
defaultLayout $ do defaultLayout $ do
addStyle [$cassius| addCassius [$cassius|
.tooltip .tooltip
color: #666 color: #666
font-style: italic font-style: italic
|] |]
addStyle [$cassius| addCassius [$cassius|
textarea.html textarea.html
width: 300px width: 300px
height: 150px height: 150px
|] |]
wrapWidget form $ \h -> [$hamlet| addWidget [$hamlet|
$maybe formFailures.res failures $maybe formFailures.res failures
%ul.errors %ul.errors
$forall failures f $forall failures f
@ -110,7 +109,7 @@ $maybe formFailures.res failures
%form!method=post!enctype=$enctype$ %form!method=post!enctype=$enctype$
$hidden$ $hidden$
%table %table
^h^ ^form^
%tr %tr
%td!colspan=2 %td!colspan=2
%input!type=submit %input!type=submit
@ -140,7 +139,7 @@ getCustomFormR = do
let b = do let b = do
b1' <- extractBody b1 b1' <- extractBody b1
b2' <- extractBody b2 b2' <- extractBody b2
addBody [$hamlet| addHamlet [$hamlet|
%p This is a custom layout. %p This is a custom layout.
%h1 Name Follows! %h1 Name Follows!
%p ^b1'^ %p ^b1'^
@ -150,7 +149,7 @@ getCustomFormR = do
(_, wform, enctype) <- runFormGet customForm (_, wform, enctype) <- runFormGet customForm
defaultLayout $ do defaultLayout $ do
form <- extractBody wform form <- extractBody wform
addBody [$hamlet| addHamlet [$hamlet|
%form %form
^form^ ^form^
%div %div

View File

@ -48,6 +48,7 @@ library
, xss-sanitize >= 0.2 && < 0.3 , xss-sanitize >= 0.2 && < 0.3
, data-default >= 0.2 && < 0.3 , data-default >= 0.2 && < 0.3
, failure >= 0.1 && < 0.2 , failure >= 0.1 && < 0.2
, containers >= 0.2 && < 0.5
exposed-modules: Yesod exposed-modules: Yesod
Yesod.Content Yesod.Content
Yesod.Dispatch Yesod.Dispatch