{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} -- | -- Language handling for Snap. -- -- Support for determining the client's prefered language using -- the Accept-Language header or using suffixes to the requested URI. module Snap.Language ( RangeMapping , getAcceptLanguage , getSuffixLanguage , switchSuffixLanguage , setContentLanguage ) where import Data.Attoparsec.ByteString.Char8(parseOnly, string, double, Parser, letter_ascii, many1, many', char, option, eitherP, sepBy, skipSpace, endOfLine) import Data.ByteString (ByteString, isSuffixOf) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import Data.Char (toLower) import Data.List (intersperse,isPrefixOf,find) import Control.Applicative ((*>),(<$>),(<*>),(<|>)) import Snap.Core (getsRequest, getRequest, putRequest, getHeader, MonadSnap, Cookie(..), addResponseCookie, modifyResponse, getCookie, setHeader, pass, rqPathInfo) import Data.Maybe (mapMaybe, listToMaybe) import Data.Map (Map, toList) import Data.Tuple (swap) range :: Parser String range = (++) <$> mletters <*> (fmap concat $ many' $ (:) <$> (char '-') <*> mletters) where mletters = many1 letter_ascii rangeval :: Parser (Maybe String, Double) rangeval = do r <- eitherP (char '*') range q <- option 1 $ string ";q=" *> double return (either (const Nothing) Just r,q) acceptLanguageParser :: Parser [(Maybe String, Double)] acceptLanguageParser = skipSpace *> (sepBy rangeval $ skipSpace *> char ',' <* skipSpace) matches :: String -> Maybe String -> Bool matches _ Nothing = True matches provided (Just requested) = (map toLower requested) `isPrefixOf` (map toLower provided) candidates :: Map String a -> [(Maybe String, Double)] -> [(a,Double)] candidates provided requested = concatMap go $ toList provided where go (range,x) = map (\(a,b) -> (x,b)) $ filter (matches range . fst) requested pickLanguage' :: Map String a -> [(Maybe String,Double)] -> Maybe a pickLanguage' provided requested = fmap fst $ foldr go Nothing $ candidates provided requested where go r' Nothing = return r' go r'@(val',q') (Just r@(val,q)) | q' > q = return r' | otherwise = return r pickLanguage :: Map String a -> ByteString -> Maybe a pickLanguage provided headerString = either (const Nothing) (pickLanguage' provided) $ parseOnly acceptLanguageParser headerString -- | Attempt to find a suitable language according to the Accept-Language -- header of the request. -- -- This handler will call pass if it cannot find a suitable language. getAcceptLanguage :: MonadSnap m => RangeMapping a -> m a getAcceptLanguage rangeMapping = do al <- getsRequest $ getHeader "Accept-Language" maybe pass return $ al >>= pickLanguage rangeMapping -- | A mapping from language ranges as defined in rfc2616 to languages in your own representation. -- -- For example: -- -- > data Language = EN | SV deriving Eq -- > -- > mapping :: RangeMapping Language -- > mapping = Map.fromList [("en-gb",EN),("sv-se",SV)] type RangeMapping a = Map String a removeSuffix :: ByteString -> ByteString -> Maybe ByteString removeSuffix suf x | suf `B.isSuffixOf` x = Just $ B.take ((B.length x) - (B.length suf)) x | otherwise = Nothing suffixes :: RangeMapping a -> [(ByteString,a)] suffixes = map go . toList where go (str,val) = (BC.pack $ '.':str,val) matchSuffix :: ByteString -> [(ByteString,a)] -> Maybe (ByteString,a) matchSuffix str sfxs = listToMaybe $ mapMaybe go sfxs where go (sfx,val) = fmap (,val) $ removeSuffix sfx str -- | Attempt to find a suitable language according to a suffix in the request URI corresponding to a language range. -- -- Will call pass if it cannot find a suitable language. -- -- If a match is found, the suffix will be removed from the URI in the request, so that you -- can later match on your resource as usual and not worry about suffixes. -- -- For example, with the following requested URI: -- -- > /resource.en-gb?param=value -- -- 'getSuffixLanguage' with our previously defined mapping will return 'EN' and 'rqPathInfo' will be changed to: -- -- > /resource?param=value getSuffixLanguage :: MonadSnap m => RangeMapping a -> m a getSuffixLanguage rangeMapping = do r <- getRequest case matchSuffix (rqPathInfo r) $ suffixes rangeMapping of Nothing -> pass Just (rqPathInfo',val) -> do putRequest $ r { rqPathInfo = rqPathInfo' } return val -- | Change, or remove, the language suffix of an URI. switchSuffixLanguage :: Eq a => RangeMapping a -> ByteString -- ^ The URI. -> Maybe a -- ^ The language to be appended to the URI, or Nothing to remove language suffix. -> ByteString switchSuffixLanguage rangeMapping uri lang = maybe (addSuffix lang path) (addSuffix lang . fst) $ matchSuffix path $ suffixes rangeMapping where (path,params) = BC.break ((==) '?') uri addSuffix lang p = B.concat [p,findSfx lang,params] findSfx Nothing = B.empty findSfx (Just l) = maybe B.empty id $ lookup l $ map swap $ suffixes rangeMapping -- | Set the Content-Language header in the response. setContentLanguage :: (Eq a, MonadSnap m) => RangeMapping a -> a -> m () setContentLanguage rangeMapping val = maybe (return ()) go $ lookup val $ map swap $ toList rangeMapping where go = modifyResponse . setHeader "Content-Language" . BC.pack