cgi-3001.3.0.3: Network/CGI/Accept.hs
module Network.CGI.Accept (
-- * Accept-X headers
Acceptable
, Accept
, Charset(..), ContentEncoding(..), Language(..)
-- * Content negotiation
, negotiate
) where
import Data.Function
import Data.List
import Data.Maybe
import Numeric
import Text.ParserCombinators.Parsec
import Network.Multipart
import Network.Multipart.Header
--
-- * Accept-X headers
--
newtype Accept a = Accept [(a, Quality)]
deriving (Show)
type Quality = Double
-- A bounded join-semilattice
class Eq a => Acceptable a where
includes :: a -> a -> Bool
{- top :: a
TODO: This method is not exported from his module, so it cannot be used by
code on the outside. It is not used inside of this module either. So,
do we actually need it?
-}
instance HeaderValue a => HeaderValue (Accept a) where
parseHeaderValue = fmap Accept $ sepBy p (lexeme (char ','))
where p = do a <- parseHeaderValue
q <- option 1 $ do _ <- lexeme $ char ';'
_ <- lexeme $ char 'q'
_ <- lexeme $ char '='
lexeme pQuality
return (a,q)
pQuality = (char '0' >> option "0" (char '.' >> many digit) >>= \ds -> return (read ("0." ++ ds ++ "0")))
<|> (char '1' >> optional (char '.' >> many (char '0')) >> return 1)
prettyHeaderValue (Accept xs) = concat $ intersperse ", " [prettyHeaderValue a ++ "; q=" ++ showQuality q | (a,q) <- xs]
where showQuality q = showFFloat (Just 3) q ""
starOrEqualTo :: String -> String -> Bool
starOrEqualTo x y = x == "*" || x == y
negotiate :: Acceptable a => [a] -> Maybe (Accept a) -> [a]
negotiate ys Nothing = ys
negotiate ys (Just xs) = reverse [ z | (q,z) <- sortBy (compare `on` fst) [ (quality xs y,y) | y <- ys], q > 0]
--testNegotiate :: (HeaderValue a, Acceptable a) => [String] -> String -> [a]
--testNegotiate ts a = negotiate [t | Just t <- map (parseM parseHeaderValue "<source>") ts] (parseM parseHeaderValue "<source>" a)
quality :: Acceptable a => Accept a -> a -> Quality
quality (Accept xs) y = fromMaybe 0 $ listToMaybe $ sort $ map snd $ sortBy (compareSpecificity `on` fst) $ filter ((`includes` y) . fst) xs
compareSpecificity :: Acceptable a => a -> a -> Ordering
compareSpecificity x y
| x `includes` y && y `includes` x = EQ
| x `includes` y = GT
| y `includes` x = LT
| otherwise = error "Non-comparable Acceptables"
--
-- ** Accept
--
instance Acceptable ContentType where
includes x y = ctType x `starOrEqualTo` ctType y
&& ctSubtype x `starOrEqualTo` ctSubtype y
&& all (hasParameter y) (ctParameters x)
-- top = ContentType "*" "*" []
hasParameter :: ContentType -> (String, String) -> Bool
hasParameter t (k,v) = maybe False (==v) $ lookup k (ctParameters t)
--
-- ** Accept-Charset
--
{-
RFC 2616 14.2:
The special value "*", if present in the Accept-Charset field, matches
every character set (including ISO-8859-1) which is not mentioned
elsewhere in the Accept-Charset field. If no "*" is present in an
Accept-Charset field, then all character sets not explicitly mentioned
get a quality value of 0, except for ISO-8859-1, which gets a quality
value of 1 if not explicitly mentioned.
If no Accept-Charset header is present, the default is that any
character set is acceptable. If an Accept-Charset header is present,
and if the server cannot send a response which is acceptable according
to the Accept-Charset header, then the server SHOULD send an error
response with the 406 (not acceptable) status code, though the sending
of an unacceptable response is also allowed.
-}
newtype Charset = Charset String
deriving (Show)
instance Eq Charset where
Charset x == Charset y = caseInsensitiveEq x y
instance Ord Charset where
Charset x `compare` Charset y = caseInsensitiveCompare x y
instance HeaderValue Charset where
parseHeaderValue = fmap Charset $ many ws1 >> lexeme p_token
prettyHeaderValue (Charset s) = s
instance Acceptable Charset where
Charset x `includes` Charset y = starOrEqualTo x y
-- top = Charset "*"
--
-- ** Accept-Encoding
--
{-
RFC 2616, section 14.3
-}
newtype ContentEncoding = ContentEncoding String
deriving (Show)
instance Eq ContentEncoding where
ContentEncoding x == ContentEncoding y = caseInsensitiveEq x y
instance Ord ContentEncoding where
ContentEncoding x `compare` ContentEncoding y = caseInsensitiveCompare x y
instance HeaderValue ContentEncoding where
parseHeaderValue = fmap ContentEncoding $ many ws1 >> lexeme p_token
prettyHeaderValue (ContentEncoding s) = s
instance Acceptable ContentEncoding where
ContentEncoding x `includes` ContentEncoding y = starOrEqualTo x y
-- top = ContentEncoding "*"
--
-- ** Accept-Language
--
newtype Language = Language String
deriving (Show)
instance Eq Language where
Language x == Language y = caseInsensitiveEq x y
instance Ord Language where
Language x `compare` Language y = caseInsensitiveCompare x y
instance HeaderValue Language where
parseHeaderValue = fmap Language $ many ws1 >> lexeme p_token
prettyHeaderValue (Language s) = s
{-
RFC 2616 14.4
A language-range matches a language-tag if it exactly equals the tag,
or if it exactly equals a prefix of the tag such that the first tag
character following the prefix is "-". The special range "*", if
present in the Accept-Language field, matches every tag not matched by
any other range present in the Accept-Language field.
-}
instance Acceptable Language where
Language x `includes` Language y =
x == "*" || x == y || (x `isPrefixOf` y && "-" `isPrefixOf` drop (length x) y)
-- top = Language "*"