module Network.CGI.Session (-- * Types Session(..) ,Sessions ,SessionName ,SessionM -- * Initialising and querying/updating ,makeSessions ,initSession ,updateSession ,sessionId -- * A simple Session monad. ,runSessionCGI ,runSession ,sessionIns ,sessionDel ,sessionGet -- * Utilities ,makeSession ,getSession) where import Control.Applicative import Control.Concurrent.MVar (MVar,modifyMVar,readMVar,modifyMVar_,newMVar) import Control.Monad.Trans (liftIO,lift) import Control.Monad.State (StateT,evalStateT,get,modify,gets) import Foreign.Marshal.Alloc (alloca) import Foreign.Storable (peek,sizeOf) import Data.Function import Data.List (nub) import Data.Map (Map) import qualified Data.Map as M import System.IO (openBinaryFile,IOMode(ReadMode),hGetBuf,hClose) import System.Random (mkStdGen,StdGen,randomRs) import qualified Network.CGI as CGI -------------------------------------------------------------------------------- -- Section: Sessions implementation -- Keywords: sessions -- Description: A poor man's implementation of sessions for CGI. -- | A simple Session monad. Recommend you define your own. type SessionM = StateT Session (CGI.CGIT IO) -- | Initialise a session state and start a F/CGI process. This is a -- bit of a pattern so I've included it here for convenience. runSessionCGI :: SessionName -> (CGI.CGI CGI.CGIResult -> IO ()) -> SessionM CGI.CGIResult -> IO () runSessionCGI name run m = do sessions <- makeSessions run $ CGI.handleErrors $ do session <- initSession name sessions runSession sessions m session -- | Simple session runner. runSession :: Sessions -> SessionM a -> Session -> CGI.CGI a runSession sessions = evalStateT -- | Session value getter. sessionGet :: Read a => String -> SessionM (Maybe a) sessionGet k = get >>= \Session{sess_values=vs} -> return $ read <$> M.lookup k vs -- | Session value inserter/updater. sessionIns :: (Read a,Show a) => String -> a -> (a -> a -> a) -> SessionM () sessionIns k v f = -- Technically the other two accessors could be defined in terms of this -- but it might confuse (me). modify $ \s@Session{sess_values = vs} -> s {sess_values = M.insertWith lf k (show v) vs } where lf = (show .) . (f `on` read) -- | Session value deleter. sessionDel :: String -> SessionM () sessionDel k = modify $ \s@Session{sess_values = vs} -> s {sess_values = M.delete k vs } -- | Session value getter. sessionId :: SessionM Integer sessionId = gets sess_id -- | A session consists of a unique id and a map. data Session = Session { sess_id :: Integer , sess_values :: Map String String } deriving (Eq,Show) -- | Sessions and unique ids are stored in an MVar. type Sessions = MVar ([Integer],Map Integer Session) -- | The cookie prefix (e.g. MYHASKELLCOOKIE). type SessionName = String -- | Make the sessions state. makeSessions :: IO Sessions makeSessions = genIds >>= newMVar . flip (,) M.empty -- | Generate random. genIds :: IO [Integer] genIds = nub . randomRs (1,1000^(20::Int)) <$> betterStdGen -- | An OK-ish random generator. betterStdGen :: IO StdGen betterStdGen = alloca $ \p -> do h <- openBinaryFile "/dev/urandom" ReadMode hGetBuf h p $ sizeOf (undefined :: Int) hClose h mkStdGen <$> peek p -- | Grab the session or create a new one. initSession :: SessionName -> Sessions -> CGI.CGI Session initSession sessionName var = do s <- getSession sessionName var case s of Just s -> return s Nothing -> makeSession sessionName var -- | Create a new session and update the Mvar. makeSession :: SessionName -> Sessions -> CGI.CGI Session makeSession sessionName var = do sess <- liftIO $ modifyMVar var $ \state@(id:ids,sessions) -> let session = Session id M.empty newState = (ids,M.insert id session sessions) in return (newState,session) CGI.setCookie (CGI.newCookie sessionName $ show $ sess_id sess) { CGI.cookiePath = Just "/" } return sess -- | Try to get the current session. getSession :: SessionName -> Sessions -> CGI.CGI (Maybe Session) getSession sessionName var = do sId <- CGI.readCookie sessionName (ids,sessions) <- liftIO $ readMVar var return $ sId >>= flip M.lookup sessions -- | Update a session in the map. updateSession :: Sessions -> Session -> CGI.CGI () updateSession var session@(Session id _) = do liftIO $ modifyMVar_ var $ \(ids,sessions) -> do return (ids,M.insert id session sessions)