-- © 2001, 2002, 2003 Peter Thiemann -- |creating, setting, reading, and deleting cookies. Cookies are type-indexed, -- i.e., there is a different cookie for each name and type. module WASH.CGI.Cookie (T, check, create, createExpiring, init, initExpiring, get, set, current, delete) where import Locale import Maybe import Prelude hiding (init) import Random import Time import WASH.CGI.BaseCombinators import WASH.CGI.CGIMonad import WASH.HTML.HTMLMonad hiding (head, div, map, span) import WASH.CGI.Types import WASH.CGI.StateItem -- |@check name@ returns the current handle to cookie @name@ if that exists. check :: (Read a, Show a, Types a) => String -> CGI (Maybe (T a)) -- |@create name initValue@ creates cookie @name@ with initial value @initValue@ -- and returns its current handle. The cookie -- expires at the end of the current run of the browser. create :: (Read a, Show a, Types a) => String -> a -> CGI (T a) -- |like 'create' with additional parameter specifying number of minutes until -- cookie expires. createExpiring :: (Read a, Show a, Types a) => String -> Int -> a -> CGI (T a) -- |@init name initValue@ returns current handle to cookie @name@. If the -- cookie did not exist before, it is created and set to @initialValue@. The cookie -- expires at the end of the current run of the browser. init :: (Read a, Show a, Types a) => String -> a -> CGI (T a) -- |@initExpiring name minutes initValue@ works like 'init' except that the -- expiration time is @minutes@ in the future. initExpiring :: (Read a, Show a, Types a) => String -> Int -> a -> CGI (T a) -- |@get handle@ returns the cookie's value if the handle is current, otherwise -- returns 'Nothing' get :: (Read a, Show a, Types a) => T a -> CGI (Maybe a) -- |@set handle newValue@ overwrites the cookie's value with @newValue@ if the -- handle is current. In that case it returns a handle which is current for -- @newValue@. Otherwise, the result is 'Nothing'. set :: (Read a, Show a, Types a) => T a -> a -> CGI (Maybe (T a)) -- |@current handle@ returns @Nothing@ if @handle@ is still current. Otherwise, it -- returns @Just newHandle@ where @newHandle@ is current for the cookie pointed to by -- @handle@. current :: (Read a, Show a, Types a) => T a -> CGI (Maybe (T a)) -- |@delete handle@ removes the cookie determined by @handle@. delete :: (Types a) => T a -> CGI () -- data P a = P { nr :: Int, vl :: a } deriving (Read, Show) t :: String -> P a -> T a t name (P i a) = T name i check name = once (unsafe_getHandle name) unsafe_getHandle name = CGI (\cgistate -> let mresult = fmap (t name) $ cookieLookup cgistate typedName myTyspec = ty (tvirtual (fromJust mresult)) typedName = makeTypedName name myTyspec in return (mresult, cgistate)) create name val = once (unsafe_init False name val Nothing) createExpiring name minutes val = once $ do timestr <- unsafe_io $ getTimeString minutes unsafe_init False name val (Just timestr) initExpiring name minutes val = once $ do timestr <- unsafe_io $ getTimeString minutes unsafe_init True name val (Just timestr) init name val = once (unsafe_init True name val Nothing) unsafe_init useExisting name val mexp = CGI (\cgistate -> case cookieLookup cgistate typedName of -- if name present and existing value is to be used -- then construct handle from map entry Just pair | useExisting -> return (t name pair, cgistate) _ -> -- extend cookie map with name=val do nonce <- randomIO let p0val = P nonce val cm' = (typedName, (Just (show p0val), mexp)) : cookieMap cgistate -- register cookie for sending cts' = typedName : cookiesToSend cgistate -- construct handle return (t name p0val, cgistate { cookieMap = cm' , cookiesToSend = cts' })) where myTyspec = ty val typedName = makeTypedName name myTyspec get handle = once (do eitherTP <- unsafe_get_current handle return $ either (const Nothing) Just eitherTP) current handle = once (do eitherTP <- unsafe_get_current handle return $ either Just (const Nothing) eitherTP) unsafe_get_current :: (Types a, Read a, Show a) => T a -> CGI (Either (T a) a) unsafe_get_current t@(T name i) = CGI (\cgistate -> case cookieLookup cgistate typedName of -- if name present and types match then construct handle from map entry (Just pair) | nr pair == i -> return (Right $ vl pair, cgistate) | otherwise -> return (Left $ T name (nr pair), cgistate) _ -> reportError "Cookie disappeared" (do let cm = cookieMap cgistate mms = lookup typedName cm mmp = if True then fmap (fmap reads . fst) mms else Just (Just [(P 0 $ tvirtual t, "")]) pre $ text $ (" " ++ typedName) pre $ text $ show cm pre $ text $ show mms pre $ text $ show mmp) cgistate) where myTyspec = ty (tvirtual t) typedName = makeTypedName name myTyspec set handle val = once (unsafe_set handle val) unsafe_set (T name i) val = CGI (\cgistate -> case cookieLookup cgistate typedName of (Just pair) | nr pair == i -> do nonce <- randomIO let pair' = if True then P nonce val else pair -- to unify their types cts = cookiesToSend cgistate cts' = if typedName `elem` cts then cts else typedName : cts cm' = (typedName, (Just (show pair'), Nothing)) : [ entry | entry@(aName, _) <- cookieMap cgistate , aName /= typedName ] return (Just (t name pair'), cgistate { cookieMap = cm' , cookiesToSend = cts' }) _ -> return (Nothing, cgistate)) where myTyspec = ty val typedName = makeTypedName name myTyspec delete t = once (unsafe_delete t) unsafe_delete t@(T name i) = CGI (\cgistate -> let cts = cookiesToSend cgistate cts' = if typedName `elem` cts then cts else typedName : cts in return ( () , cgistate { cookieMap = (typedName, (Nothing, Nothing)) : cookieMap cgistate , cookiesToSend = cts' })) where myTyspec = ty (tvirtual t) typedName = makeTypedName name myTyspec cookieLookup :: (Show a, Read a) => CGIState -> String -> Maybe (P a) cookieLookup cgistate typedName = let cm = cookieMap cgistate fn = lookup typedName cm -- Maybe (Maybe String, Maybe String) cn = fmap (fmap reads . fst) fn -- Maybe (Maybe [(P a, String)]) checkType [] = Nothing checkType ((pair,_):_) = Just pair in cn >>= \mtp -> mtp >>= checkType makeTypedName :: String -> TySpec -> String makeTypedName s tys = s ++ '-' : tid tys "" -- getTimeString :: Int -> IO String getTimeString minutes = do now <- getClockTime let expireAt = addToClockTime TimeDiff {tdYear = 0, tdMonth = 0, tdDay = 0, tdHour = 0, tdMin = minutes, tdSec = 0, tdPicosec = 0} now -- Wdy, DD-Mon-YYYY HH:MM:SS GMT fmt = "%a, %d-%b-%Y %H:%M:%S GMT" loc = defaultTimeLocale return $ formatCalendarTime loc fmt (toUTCTime expireAt)