packages feed

Spock-core-0.14.0.1: src/Web/Spock/Internal/Cookies.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

module Web.Spock.Internal.Cookies where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time
import qualified Network.HTTP.Types.URI as URI (urlDecode, urlEncode)
import qualified Web.Cookie as C

#if MIN_VERSION_base(4,8,0)
#else
import Control.Applicative
#endif

-- | Cookie settings
data CookieSettings = CookieSettings
  { -- | cookie expiration setting, see 'CookieEOL'
    cs_EOL :: CookieEOL,
    -- | a path for the cookie
    cs_path :: Maybe BS.ByteString,
    -- | a domain for the cookie. 'Nothing' means no domain is set
    cs_domain :: Maybe BS.ByteString,
    -- | whether the cookie should be set as HttpOnly
    cs_HTTPOnly :: Bool,
    -- | whether the cookie should be marked secure (sent over HTTPS only)
    cs_secure :: Bool
  }

-- | Setting cookie expiration
data CookieEOL
  = -- | a point in time in UTC until the cookie is valid
    CookieValidUntil UTCTime
  | -- | a period (in seconds) for which the cookie is valid
    CookieValidFor NominalDiffTime
  | -- | the cookie expires with the browser session
    CookieValidForSession
  | -- | the cookie will have an expiration date in the far future
    CookieValidForever

-- | Default cookie settings, equals
--
-- > CookieSettings
-- >   { cs_EOL      = CookieValidForSession
-- >   , cs_HTTPOnly = False
-- >   , cs_secure   = False
-- >   , cs_domain   = Nothing
-- >   , cs_path     = Just "/"
-- >   }
defaultCookieSettings :: CookieSettings
defaultCookieSettings =
  CookieSettings
    { cs_EOL = CookieValidForSession,
      cs_HTTPOnly = False,
      cs_secure = False,
      cs_domain = Nothing,
      cs_path = Just "/"
    }

parseCookies :: BS.ByteString -> [(T.Text, T.Text)]
parseCookies =
  map (\(a, b) -> (T.decodeUtf8 a, T.decodeUtf8 $ URI.urlDecode True b))
    . C.parseCookies

generateCookieHeaderString ::
  T.Text ->
  T.Text ->
  CookieSettings ->
  UTCTime ->
  BS.ByteString
generateCookieHeaderString name value cs now =
  let farFuture =
        -- don't forget to bump this ...
        UTCTime (fromGregorian 2030 1 1) 0
      (expire, maxAge) =
        case cs_EOL cs of
          CookieValidUntil t ->
            (Just t, Just (t `diffUTCTime` now))
          CookieValidFor x ->
            (Just (x `addUTCTime` now), Just x)
          CookieValidForSession ->
            (Nothing, Nothing)
          CookieValidForever ->
            (Just farFuture, Just 2147483000)
      adjustMaxAge t =
        if t < 0 then 0 else t
      cookieVal =
        C.def
          { C.setCookieName = T.encodeUtf8 name,
            C.setCookieValue = URI.urlEncode True $ T.encodeUtf8 value,
            C.setCookiePath = cs_path cs,
            C.setCookieExpires = expire,
            C.setCookieMaxAge = (fromRational . adjustMaxAge . toRational) <$> maxAge,
            C.setCookieDomain = cs_domain cs,
            C.setCookieHttpOnly = cs_HTTPOnly cs,
            C.setCookieSecure = cs_secure cs
          }
   in renderCookie cookieVal

renderCookie :: C.SetCookie -> BS.ByteString
renderCookie = BSL.toStrict . B.toLazyByteString . C.renderSetCookie