packages feed

salvia-0.1: src/Network/Protocol/Cookie.hs

{- |
For more information: http://www.ietf.org/rfc/rfc2109.txt
-}

module Network.Protocol.Cookie (
    Cookie (..)
  , empty

  , Cookies
  , cookies
  , cookie
  , showCookies

  , parseCookies
  ) where

import Control.Applicative hiding (empty)
import Control.Monad ()
import Data.Char (toLower)
import Data.List (intercalate)
import Misc.Misc (intersperseS, (@@), trim)
import Network.Protocol.Uri (URI)
import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
import qualified Data.Map as M


{- |
The `Cookie` data type containg one key/value pair with all the (potentially
optional) meta-data.
-}

data Cookie =
  Cookie {
    name       :: String
  , value      :: String
  , comment    :: Maybe String
  , commentURL :: Maybe URI
  , discard    :: Bool
  , domain     :: Maybe String
  , maxAge     :: Maybe Int
  , expires    :: Maybe String
  , path       :: Maybe String
  , port       :: [Int]
  , secure     :: Bool
  , version    :: Int
  }

{- | Create an empty cookie. -}

empty :: Cookie
empty = Cookie {
    name       = ""
  , value      = ""
  , comment    = Nothing
  , commentURL = Nothing
  , discard    = False
  , domain     = Nothing
  , maxAge     = Nothing
  , expires    = Nothing
  , path       = Nothing
  , port       = []
  , secure     = False
  , version    = 0
  }

{- |
A collection of multiple cookies. These can all be set in one single HTTP
/Set-Cookie/ header field.
-}

type Cookies = M.Map String Cookie

{- |
Convert a list of cookies into a cookie mapping. The name will be used as the
key, the cookie itself as the value.
-}

cookies :: [Cookie] -> Cookies
cookies = M.fromList . map (\a -> (name a, a))

{- | Case-insensitive way of getting a cookie out of a collection by name. -}

cookie :: String -> Cookies -> Maybe Cookie
cookie n = M.lookup (map toLower n)

-------[ cookie show instance ]------------------------------------------------

instance Show Cookie where
  show = flip showsCookie ""

-- Show a semicolon separated list of attribute/value pairs. Only meta pairs
-- with significant values will be pretty printed.
showsCookie :: Cookie -> ShowS
showsCookie c =
    pair     (name c)     (value c)
  . opt      "comment"    (comment c)
  . opt      "commentURL" (fmap show $ commentURL c)
  . bool     "discard"    (discard c)
  . opt      "domain"     (domain c)
  . opt      "maxAge"     (fmap show $ maxAge c)
  . opt      "expires"    (expires c)
  . opt      "path"       (path c)
  . list     "port"       (map show $ port c)
  . bool     "secure"     (secure c)
  . opt      "version"    (optval $ version c)
  where
    attr a       = showString a
    val v        = showString ("=" ++ v)
    end          = showString "; "
    single a     = attr a . end
    pair a v     = attr a . val v . end
    opt a        = maybe id (pair a)
    list _ []    = id
    list a xs    = pair a $ intercalate "," xs
    bool _ False = id
    bool a True  = single a
    optval 0     = Nothing
    optval i     = Just (show i)

{- | Show multiple cookies, pretty printed using a comma separator. -}

showCookies :: Cookies -> String
showCookies = ($"")
  . intersperseS (showString ", ")
  . map (shows . snd)
  . M.toList

-------[ cookie parser ]-------------------------------------------------------

{- |
Parse a set of cookie values and turn this into a collection of real cookies.
As the specification states, only the name, value, domain, path and port will
be recognized.
-}

parseCookies :: String -> Maybe Cookies
parseCookies = fmap cookies . (pCookie @@)

pCookie :: GenParser Char st [Cookie]
pCookie = map ck <$> pCookieValues
  where
    ck ((n, v), m) =
      empty {
        name   = n
      , value  = v
      , domain = "$domain" `M.lookup` m
      , path   = "$path"   `M.lookup` m
      , port   = maybe [] pPorts $
                 "$port"   `M.lookup` m
      }

-- Parse a list of comma separated portnumbers. Returns a list of integers or
-- an empty list on failure.
pPorts :: String -> [Int]
pPorts = either (const []) id . parse p ""
   where
    p = char '"'
     *> sepBy1 (read <$> many1 digit) (char ',')
     <* char '"'

-- Parse a collection of cookie name/value pairs.
pCookieValues :: GenParser Char st [((String, String), M.Map String String)]
pCookieValues =
      flip sepBy1 sep ((,)
  <$> pair key
  <*> (M.fromList
  <$> filter (not . null . fst)
  <$> many (try (sep *> pair skey))))
  where
    f      = trim . map toLower
    key    = f <$> many1 (noneOf "=;")
    skey   = f <$> ((:) <$> char '$' <*> many (noneOf "=;"))
    val    = trim <$> many (noneOf ";")
    pair k = (,) <$> k <*> (char '=' *> val)
    sep    = char ';' *> many (oneOf " \t\r\n")