{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Yesod.Core.Handler
(
HandlerT
, HandlerFor
, getYesod
, getsYesod
, getUrlRender
, getUrlRenderParams
, getPostParams
, getCurrentRoute
, getRequest
, waiRequest
, runRequestBody
, rawRequestBody
, RequestBodyContents
, YesodRequest (..)
, FileInfo
, fileName
, fileContentType
, fileSource
, fileSourceByteString
, fileMove
, languages
, lookupGetParam
, lookupPostParam
, lookupCookie
, lookupFile
, lookupHeader
, lookupBasicAuth
, lookupBearerAuth
, lookupGetParams
, lookupPostParams
, lookupCookies
, lookupFiles
, lookupHeaders
, respond
, respondSource
, sendChunk
, sendFlush
, sendChunkBS
, sendChunkLBS
, sendChunkText
, sendChunkLazyText
, sendChunkHtml
, RedirectUrl (..)
, redirect
, redirectWith
, redirectToPost
, Fragment(..)
, notFound
, badMethod
, notAuthenticated
, permissionDenied
, permissionDeniedI
, invalidArgs
, invalidArgsI
, sendFile
, sendFilePart
, sendResponse
, sendResponseStatus
, sendStatusJSON
, sendResponseCreated
, sendResponseNoContent
, sendWaiResponse
, sendWaiApplication
, sendRawResponse
, sendRawResponseNoConduit
, notModified
, selectRep
, provideRep
, provideRepType
, ProvidedRep
, setCookie
, getExpires
, deleteCookie
, addHeader
, setHeader
, replaceOrAddHeader
, setLanguage
, addContentDispositionFileName
, cacheSeconds
, neverExpires
, alreadyExpired
, expiresAt
, setEtag
, setWeakEtag
, SessionMap
, lookupSession
, lookupSessionBS
, getSession
, setSession
, setSessionBS
, deleteSession
, clearSession
, setUltDest
, setUltDestCurrent
, setUltDestReferer
, redirectUltDest
, clearUltDest
, addMessage
, addMessageI
, getMessages
, setMessage
, setMessageI
, getMessage
, SubHandlerFor
, getSubYesod
, getRouteToParent
, getSubCurrentRoute
, hamletToRepHtml
, giveUrlRenderer
, withUrlRenderer
, newIdent
, handlerToIO
, forkHandler
, getMessageRender
, cached
, cacheGet
, cacheSet
, cachedBy
, cacheByGet
, cacheBySet
, setCsrfCookie
, setCsrfCookieWithCookie
, defaultCsrfCookieName
, checkCsrfHeaderNamed
, hasValidCsrfHeaderNamed
, defaultCsrfHeaderName
, hasValidCsrfParamNamed
, checkCsrfParamNamed
, defaultCsrfParamName
, checkCsrfHeaderOrParam
) where
import Data.Time (UTCTime, addUTCTime,
getCurrentTime)
import Yesod.Core.Internal.Request (langKey, mkFileInfoFile,
mkFileInfoLBS, mkFileInfoSource)
import Control.Applicative ((<|>))
import qualified Data.CaseInsensitive as CI
import Control.Exception (evaluate, SomeException, throwIO)
import Control.Exception (handle)
import Control.Monad (void, liftM, unless)
import qualified Control.Monad.Trans.Writer as Writer
import UnliftIO (MonadIO, liftIO, MonadUnliftIO, withRunInIO)
import qualified Network.HTTP.Types as H
import qualified Network.Wai as W
import Network.Wai.Middleware.HttpAuth
( extractBasicAuth, extractBearerAuth )
import Control.Monad.Trans.Class (lift)
import Data.Aeson (ToJSON(..))
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With, encodeUtf8, decodeUtf8)
import Data.Text.Encoding.Error (lenientDecode)
import qualified Data.Text.Lazy as TL
import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
import Text.Hamlet (Html, HtmlUrl, hamlet)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import qualified Data.Map as Map
import qualified Data.HashMap.Strict as HM
import Data.ByteArray (constEq)
import Control.Arrow ((***))
import qualified Data.ByteString.Char8 as S8
import Data.Monoid (Endo (..))
import Data.Text (Text)
import qualified Network.Wai.Parse as NWP
import Text.Shakespeare.I18N (RenderMessage (..))
import Web.Cookie (SetCookie (..), defaultSetCookie)
import Yesod.Core.Content (ToTypedContent (..), simpleContentType, contentTypeTypes, HasContentType (..), ToContent (..), ToFlushBuilder (..))
import Yesod.Core.Internal.Util (formatRFC1123)
import Text.Blaze.Html (preEscapedToHtml, toHtml)
import qualified Data.IORef as I
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Typeable (Typeable)
import Web.PathPieces (PathPiece(..))
import Yesod.Core.Class.Handler
import Yesod.Core.Types
import Yesod.Routes.Class (Route)
import Data.ByteString.Builder (Builder)
import Data.CaseInsensitive (CI, original)
import qualified Data.Conduit.List as CL
import Control.Monad.Trans.Resource (MonadResource, InternalState, runResourceT, withInternalState, getInternalState, liftResourceT, resourceForkIO)
import qualified System.PosixCompat.Files as PC
import Conduit ((.|), runConduit, sinkLazy)
import Data.Conduit (ConduitT, transPipe, Flush (Flush), yield, Void)
import qualified Yesod.Core.TypeCache as Cache
import qualified Data.Word8 as W8
import qualified Data.Foldable as Fold
import Control.Monad.Logger (MonadLogger, logWarnS)
type HandlerT site (m :: * -> *) = HandlerFor site
{-# DEPRECATED HandlerT "Use HandlerFor directly" #-}
get :: MonadHandler m => m GHState
get = liftHandler $ HandlerFor $ I.readIORef . handlerState
put :: MonadHandler m => GHState -> m ()
put x = liftHandler $ HandlerFor $ flip I.writeIORef x . handlerState
modify :: MonadHandler m => (GHState -> GHState) -> m ()
modify f = liftHandler $ HandlerFor $ flip I.modifyIORef f . handlerState
tell :: MonadHandler m => Endo [Header] -> m ()
tell hs = modify $ \g -> g { ghsHeaders = ghsHeaders g `mappend` hs }
handlerError :: MonadHandler m => HandlerContents -> m a
handlerError = liftIO . throwIO
hcError :: MonadHandler m => ErrorResponse -> m a
hcError = handlerError . HCError
getRequest :: MonadHandler m => m YesodRequest
getRequest = liftHandler $ HandlerFor $ return . handlerRequest
runRequestBody :: MonadHandler m => m RequestBodyContents
runRequestBody = do
HandlerData
{ handlerEnv = RunHandlerEnv {..}
, handlerRequest = req
} <- liftHandler $ HandlerFor return
let len = W.requestBodyLength $ reqWaiRequest req
upload = rheUpload len
x <- get
case ghsRBC x of
Just rbc -> return rbc
Nothing -> do
rr <- waiRequest
internalState <- liftResourceT getInternalState
rbc <- liftIO $ rbHelper upload rr internalState
put x { ghsRBC = Just rbc }
return rbc
rbHelper :: FileUpload -> W.Request -> InternalState -> IO RequestBodyContents
rbHelper upload req internalState =
case upload of
FileUploadMemory s -> rbHelper' s mkFileInfoLBS req
FileUploadDisk s -> rbHelper' (s internalState) mkFileInfoFile req
FileUploadSource s -> rbHelper' s mkFileInfoSource req
rbHelper' :: NWP.BackEnd x
-> (Text -> Text -> x -> FileInfo)
-> W.Request
-> IO ([(Text, Text)], [(Text, FileInfo)])
rbHelper' backend mkFI req =
(map fix1 *** mapMaybe fix2) <$> NWP.parseRequestBody backend req
where
fix1 = go *** go
fix2 (x, NWP.FileInfo a' b c)
| S.null a = Nothing
| otherwise = Just (go x, mkFI (go a) (go b) c)
where
a
| S.length a' < 2 = a'
| S8.head a' == '"' && S8.last a' == '"' = S.tail $ S.init a'
| S8.head a' == '\'' && S8.last a' == '\'' = S.tail $ S.init a'
| otherwise = a'
go = decodeUtf8With lenientDecode
askHandlerEnv :: MonadHandler m => m (RunHandlerEnv (HandlerSite m) (HandlerSite m))
askHandlerEnv = liftHandler $ HandlerFor $ return . handlerEnv
getYesod :: MonadHandler m => m (HandlerSite m)
getYesod = rheSite <$> askHandlerEnv
getsYesod :: MonadHandler m => (HandlerSite m -> a) -> m a
getsYesod f = (f . rheSite) <$> askHandlerEnv
getUrlRender :: MonadHandler m => m (Route (HandlerSite m) -> Text)
getUrlRender = do
x <- rheRender <$> askHandlerEnv
return $ flip x []
getUrlRenderParams
:: MonadHandler m
=> m (Route (HandlerSite m) -> [(Text, Text)] -> Text)
getUrlRenderParams = rheRender <$> askHandlerEnv
getPostParams
:: MonadHandler m
=> m [(Text, Text)]
getPostParams = do
reqBodyContent <- runRequestBody
return $ fst reqBodyContent
getCurrentRoute :: MonadHandler m => m (Maybe (Route (HandlerSite m)))
getCurrentRoute = rheRoute <$> askHandlerEnv
handlerToIO :: MonadIO m => HandlerFor site (HandlerFor site a -> m a)
handlerToIO =
HandlerFor $ \oldHandlerData -> do
let newReq = oldReq { reqWaiRequest = newWaiReq }
where
oldReq = handlerRequest oldHandlerData
oldWaiReq = reqWaiRequest oldReq
newWaiReq = oldWaiReq { W.requestBody = return mempty
, W.requestBodyLength = W.KnownLength 0
}
oldEnv = handlerEnv oldHandlerData
newState <- liftIO $ do
oldState <- I.readIORef (handlerState oldHandlerData)
return $ oldState { ghsRBC = Nothing
, ghsIdent = 1
, ghsCache = mempty
, ghsCacheBy = mempty
, ghsHeaders = mempty }
liftIO $ evaluate (newReq `seq` oldEnv `seq` newState `seq` ())
return $ \(HandlerFor f) ->
liftIO $
runResourceT $ withInternalState $ \resState -> do
newStateIORef <- liftIO (I.newIORef newState)
let newHandlerData =
HandlerData
{ handlerRequest = newReq
, handlerEnv = oldEnv
, handlerState = newStateIORef
, handlerResource = resState
}
liftIO (f newHandlerData)
forkHandler :: (SomeException -> HandlerFor site ())
-> HandlerFor site ()
-> HandlerFor site ()
forkHandler onErr handler = do
yesRunner <- handlerToIO
void $ liftResourceT $ resourceForkIO $
liftIO $ handle (yesRunner . onErr) (yesRunner handler)
redirect :: (MonadHandler m, RedirectUrl (HandlerSite m) url)
=> url -> m a
redirect url = do
req <- waiRequest
let status =
if W.httpVersion req == H.http11
then H.status303
else H.status302
redirectWith status url
redirectWith :: (MonadHandler m, RedirectUrl (HandlerSite m) url)
=> H.Status
-> url
-> m a
redirectWith status url = do
urlText <- toTextUrl url
handlerError $ HCRedirect status urlText
ultDestKey :: Text
ultDestKey = "_ULT"
setUltDest :: (MonadHandler m, RedirectUrl (HandlerSite m) url)
=> url
-> m ()
setUltDest url = do
urlText <- toTextUrl url
setSession ultDestKey urlText
setUltDestCurrent :: MonadHandler m => m ()
setUltDestCurrent = do
route <- getCurrentRoute
case route of
Nothing -> return ()
Just r -> do
gets' <- reqGetParams <$> getRequest
setUltDest (r, gets')
setUltDestReferer :: MonadHandler m => m ()
setUltDestReferer = do
mdest <- lookupSession ultDestKey
maybe
(waiRequest >>= maybe (return ()) setUltDestBS . lookup "referer" . W.requestHeaders)
(const $ return ())
mdest
where
setUltDestBS = setUltDest . T.pack . S8.unpack
redirectUltDest :: (RedirectUrl (HandlerSite m) url, MonadHandler m)
=> url
-> m a
redirectUltDest defaultDestination = do
mdest <- lookupSession ultDestKey
deleteSession ultDestKey
maybe (redirect defaultDestination) redirect mdest
clearUltDest :: MonadHandler m => m ()
clearUltDest = deleteSession ultDestKey
msgKey :: Text
msgKey = "_MSG"
addMessage :: MonadHandler m
=> Text
-> Html
-> m ()
addMessage status msg = do
val <- lookupSessionBS msgKey
setSessionBS msgKey $ addMsg val
where
addMsg = maybe msg' (S.append msg' . S.cons W8._nul)
msg' = S.append
(encodeUtf8 status)
(W8._nul `S.cons` L.toStrict (renderHtml msg))
addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg)
=> Text -> msg -> m ()
addMessageI status msg = do
mr <- getMessageRender
addMessage status $ toHtml $ mr msg
getMessages :: MonadHandler m => m [(Text, Html)]
getMessages = do
bs <- lookupSessionBS msgKey
let ms = maybe [] enlist bs
deleteSession msgKey
return ms
where
enlist = pairup . S.split W8._nul
pairup [] = []
pairup [_] = []
pairup (s:v:xs) = (decode s, preEscapedToHtml (decode v)) : pairup xs
decode = decodeUtf8With lenientDecode
setMessage :: MonadHandler m => Html -> m ()
setMessage = addMessage ""
setMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg)
=> msg -> m ()
setMessageI = addMessageI ""
getMessage :: MonadHandler m => m (Maybe Html)
getMessage = fmap (fmap snd . listToMaybe) getMessages
sendFile :: MonadHandler m => ContentType -> FilePath -> m a
sendFile ct fp = handlerError $ HCSendFile ct fp Nothing
sendFilePart :: MonadHandler m
=> ContentType
-> FilePath
-> Integer
-> Integer
-> m a
sendFilePart ct fp off count = do
fs <- liftIO $ PC.getFileStatus fp
handlerError $ HCSendFile ct fp $ Just W.FilePart
{ W.filePartOffset = off
, W.filePartByteCount = count
, W.filePartFileSize = fromIntegral $ PC.fileSize fs
}
sendResponse :: (MonadHandler m, ToTypedContent c) => c -> m a
sendResponse = handlerError . HCContent H.status200 . toTypedContent
sendResponseStatus :: (MonadHandler m, ToTypedContent c) => H.Status -> c -> m a
sendResponseStatus s = handlerError . HCContent s . toTypedContent
sendStatusJSON :: (MonadHandler m, ToJSON c) => H.Status -> c -> m a
sendStatusJSON s v = sendResponseStatus s (toEncoding v)
sendResponseCreated :: MonadHandler m => Route (HandlerSite m) -> m a
sendResponseCreated url = do
r <- getUrlRender
handlerError $ HCCreated $ r url
sendResponseNoContent :: MonadHandler m => m a
sendResponseNoContent = sendWaiResponse $ W.responseBuilder H.status204 [] mempty
sendWaiResponse :: MonadHandler m => W.Response -> m b
sendWaiResponse = handlerError . HCWai
sendWaiApplication :: MonadHandler m => W.Application -> m b
sendWaiApplication = handlerError . HCWaiApp
sendRawResponseNoConduit
:: (MonadHandler m, MonadUnliftIO m)
=> (IO S8.ByteString -> (S8.ByteString -> IO ()) -> m ())
-> m a
sendRawResponseNoConduit raw = withRunInIO $ \runInIO ->
liftIO $ throwIO $ HCWai $ flip W.responseRaw fallback
$ \src sink -> void $ runInIO (raw src sink)
where
fallback = W.responseLBS H.status500 [("Content-Type", "text/plain")]
"sendRawResponse: backend does not support raw responses"
sendRawResponse
:: (MonadHandler m, MonadUnliftIO m)
=> (ConduitT () S8.ByteString IO () -> ConduitT S8.ByteString Void IO () -> m ())
-> m a
sendRawResponse raw = withRunInIO $ \runInIO ->
liftIO $ throwIO $ HCWai $ flip W.responseRaw fallback
$ \src sink -> void $ runInIO $ raw (src' src) (CL.mapM_ sink)
where
fallback = W.responseLBS H.status500 [("Content-Type", "text/plain")]
"sendRawResponse: backend does not support raw responses"
src' src = do
bs <- liftIO src
unless (S.null bs) $ do
yield bs
src' src
notModified :: MonadHandler m => m a
notModified = sendWaiResponse $ W.responseBuilder H.status304 [] mempty
notFound :: MonadHandler m => m a
notFound = hcError NotFound
badMethod :: MonadHandler m => m a
badMethod = do
w <- waiRequest
hcError $ BadMethod $ W.requestMethod w
notAuthenticated :: MonadHandler m => m a
notAuthenticated = hcError NotAuthenticated
permissionDenied :: MonadHandler m => Text -> m a
permissionDenied = hcError . PermissionDenied
permissionDeniedI :: (RenderMessage (HandlerSite m) msg, MonadHandler m)
=> msg
-> m a
permissionDeniedI msg = do
mr <- getMessageRender
permissionDenied $ mr msg
invalidArgs :: MonadHandler m => [Text] -> m a
invalidArgs = hcError . InvalidArgs
invalidArgsI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => [msg] -> m a
invalidArgsI msg = do
mr <- getMessageRender
invalidArgs $ map mr msg
setCookie :: MonadHandler m => SetCookie -> m ()
setCookie sc = do
addHeaderInternal (DeleteCookie name path)
addHeaderInternal (AddCookie sc)
where name = setCookieName sc
path = maybe "/" id (setCookiePath sc)
getExpires :: MonadIO m
=> Int
-> m UTCTime
getExpires m = do
now <- liftIO getCurrentTime
return $ fromIntegral (m * 60) `addUTCTime` now
deleteCookie :: MonadHandler m
=> Text
-> Text
-> m ()
deleteCookie a = addHeaderInternal . DeleteCookie (encodeUtf8 a) . encodeUtf8
setLanguage :: MonadHandler m => Text -> m ()
setLanguage = setSession langKey
addContentDispositionFileName :: MonadHandler m => T.Text -> m ()
addContentDispositionFileName fileName
= addHeader "Content-Disposition" $ rfc6266Utf8FileName fileName
rfc6266Utf8FileName :: T.Text -> T.Text
rfc6266Utf8FileName fileName = "attachment; filename*=UTF-8''" `mappend` decodeUtf8 (H.urlEncode True (encodeUtf8 fileName))
addHeader :: MonadHandler m => Text -> Text -> m ()
addHeader a = addHeaderInternal . Header (CI.mk $ encodeUtf8 a) . encodeUtf8
setHeader :: MonadHandler m => Text -> Text -> m ()
setHeader = addHeader
{-# DEPRECATED setHeader "Please use addHeader instead" #-}
replaceOrAddHeader :: MonadHandler m => Text -> Text -> m ()
replaceOrAddHeader a b =
modify $ \g -> g {ghsHeaders = replaceHeader (ghsHeaders g)}
where
repHeader = Header (CI.mk $ encodeUtf8 a) (encodeUtf8 b)
sameHeaderName :: Header -> Header -> Bool
sameHeaderName (Header n1 _) (Header n2 _) = n1 == n2
sameHeaderName _ _ = False
replaceIndividualHeader :: [Header] -> [Header]
replaceIndividualHeader [] = [repHeader]
replaceIndividualHeader xs = aux xs []
where
aux [] acc = acc ++ [repHeader]
aux (x:xs') acc =
if sameHeaderName repHeader x
then acc ++
[repHeader] ++
(filter (\header -> not (sameHeaderName header repHeader)) xs')
else aux xs' (acc ++ [x])
replaceHeader :: Endo [Header] -> Endo [Header]
replaceHeader endo =
let allHeaders :: [Header] = appEndo endo []
in Endo (\rest -> replaceIndividualHeader allHeaders ++ rest)
cacheSeconds :: MonadHandler m => Int -> m ()
cacheSeconds i = setHeader "Cache-Control" $ T.concat
[ "max-age="
, T.pack $ show i
, ", public"
]
neverExpires :: MonadHandler m => m ()
neverExpires = do
setHeader "Expires" . rheMaxExpires =<< askHandlerEnv
cacheSeconds oneYear
where
oneYear :: Int
oneYear = 60 * 60 * 24 * 365
alreadyExpired :: MonadHandler m => m ()
alreadyExpired = setHeader "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
expiresAt :: MonadHandler m => UTCTime -> m ()
expiresAt = setHeader "Expires" . formatRFC1123
data Etag
= WeakEtag !S.ByteString
| StrongEtag !S.ByteString
| InvalidEtag !S.ByteString
deriving (Show, Eq)
setEtag :: MonadHandler m => Text -> m ()
setEtag etag = do
mmatch <- lookupHeader "if-none-match"
let matches = maybe [] parseMatch mmatch
baseTag = encodeUtf8 etag
strongTag = StrongEtag baseTag
badTag = InvalidEtag baseTag
if any (\tag -> tag == strongTag || tag == badTag) matches
then notModified
else addHeader "etag" $ T.concat ["\"", etag, "\""]
parseMatch :: S.ByteString -> [Etag]
parseMatch =
map clean . S.split W8._comma
where
clean = classify . fst . S.spanEnd W8.isSpace . S.dropWhile W8.isSpace
classify bs
| S.length bs >= 2 && S.head bs == W8._quotedbl && S.last bs == W8._quotedbl
= StrongEtag $ S.init $ S.tail bs
| S.length bs >= 4 &&
S.head bs == W8._W &&
S.index bs 1 == W8._slash &&
S.index bs 2 == W8._quotedbl &&
S.last bs == W8._quotedbl
= WeakEtag $ S.init $ S.drop 3 bs
| otherwise = InvalidEtag bs
setWeakEtag :: MonadHandler m => Text -> m ()
setWeakEtag etag = do
mmatch <- lookupHeader "if-none-match"
let matches = maybe [] parseMatch mmatch
if WeakEtag (encodeUtf8 etag) `elem` matches
then notModified
else addHeader "etag" $ T.concat ["W/\"", etag, "\""]
setSession :: MonadHandler m
=> Text
-> Text
-> m ()
setSession k = setSessionBS k . encodeUtf8
setSessionBS :: MonadHandler m
=> Text
-> S.ByteString
-> m ()
setSessionBS k = modify . modSession . Map.insert k
deleteSession :: MonadHandler m => Text -> m ()
deleteSession = modify . modSession . Map.delete
clearSession :: MonadHandler m => m ()
clearSession = modify $ \x -> x { ghsSession = Map.empty }
modSession :: (SessionMap -> SessionMap) -> GHState -> GHState
modSession f x = x { ghsSession = f $ ghsSession x }
addHeaderInternal :: MonadHandler m => Header -> m ()
addHeaderInternal = tell . Endo . (:)
class RedirectUrl master a where
toTextUrl :: (MonadHandler m, HandlerSite m ~ master) => a -> m Text
instance RedirectUrl master Text where
toTextUrl = return
instance RedirectUrl master String where
toTextUrl = toTextUrl . T.pack
instance RedirectUrl master (Route master) where
toTextUrl url = do
r <- getUrlRender
return $ r url
instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, [(key, val)]) where
toTextUrl (url, params) = do
r <- getUrlRenderParams
return $ r url params
instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, Map.Map key val) where
toTextUrl (url, params) = toTextUrl (url, Map.toList params)
data Fragment a b = a :#: b deriving (Show, Typeable)
instance (RedirectUrl master a, PathPiece b) => RedirectUrl master (Fragment a b) where
toTextUrl (a :#: b) = (\ua -> T.concat [ua, "#", toPathPiece b]) <$> toTextUrl a
lookupSession :: MonadHandler m => Text -> m (Maybe Text)
lookupSession = (fmap . fmap) (decodeUtf8With lenientDecode) . lookupSessionBS
lookupSessionBS :: MonadHandler m => Text -> m (Maybe S.ByteString)
lookupSessionBS n = do
m <- fmap ghsSession get
return $ Map.lookup n m
getSession :: MonadHandler m => m SessionMap
getSession = fmap ghsSession get
newIdent :: MonadHandler m => m Text
newIdent = do
x <- get
let i' = ghsIdent x + 1
put x { ghsIdent = i' }
return $ T.pack $ "hident" ++ show i'
redirectToPost :: (MonadHandler m, RedirectUrl (HandlerSite m) url)
=> url
-> m a
redirectToPost url = do
urlText <- toTextUrl url
req <- getRequest
withUrlRenderer [hamlet|
$newline never
$doctype 5
<html>
<head>
<title>Redirecting...
<body>
<form id="form" method="post" action=#{urlText}>
$maybe token <- reqToken req
<input type=hidden name=#{defaultCsrfParamName} value=#{token}>
<noscript>
<p>Javascript has been disabled; please click on the button below to be redirected.
<input type="submit" value="Continue">
<script>
window.onload = function() { document.getElementById('form').submit(); };
|] >>= sendResponse
hamletToRepHtml :: MonadHandler m => HtmlUrl (Route (HandlerSite m)) -> m Html
hamletToRepHtml = withUrlRenderer
{-# DEPRECATED hamletToRepHtml "Use withUrlRenderer instead" #-}
giveUrlRenderer :: MonadHandler m
=> ((Route (HandlerSite m) -> [(Text, Text)] -> Text) -> output)
-> m output
giveUrlRenderer = withUrlRenderer
{-# DEPRECATED giveUrlRenderer "Use withUrlRenderer instead" #-}
withUrlRenderer :: MonadHandler m
=> ((Route (HandlerSite m) -> [(Text, Text)] -> Text) -> output)
-> m output
withUrlRenderer f = do
render <- getUrlRenderParams
return $ f render
waiRequest :: MonadHandler m => m W.Request
waiRequest = reqWaiRequest <$> getRequest
getMessageRender :: (MonadHandler m, RenderMessage (HandlerSite m) message)
=> m (message -> Text)
getMessageRender = do
env <- askHandlerEnv
l <- languages
return $ renderMessage (rheSite env) l
cached :: (MonadHandler m, Typeable a)
=> m a
-> m a
cached action = do
cache <- ghsCache <$> get
eres <- Cache.cached cache action
case eres of
Right res -> return res
Left (newCache, res) -> do
gs <- get
let merged = newCache `HM.union` ghsCache gs
put $ gs { ghsCache = merged }
return res
cacheGet :: (MonadHandler m, Typeable a)
=> m (Maybe a)
cacheGet = do
cache <- ghsCache <$> get
pure $ Cache.cacheGet cache
cacheSet :: (MonadHandler m, Typeable a)
=> a
-> m ()
cacheSet value = do
gs <- get
let cache = ghsCache gs
newCache = Cache.cacheSet value cache
put $ gs { ghsCache = newCache }
cachedBy :: (MonadHandler m, Typeable a) => S.ByteString -> m a -> m a
cachedBy k action = do
cache <- ghsCacheBy <$> get
eres <- Cache.cachedBy cache k action
case eres of
Right res -> return res
Left (newCache, res) -> do
gs <- get
let merged = newCache `HM.union` ghsCacheBy gs
put $ gs { ghsCacheBy = merged }
return res
cacheByGet :: (MonadHandler m, Typeable a)
=> S.ByteString
-> m (Maybe a)
cacheByGet key = do
cache <- ghsCacheBy <$> get
pure $ Cache.cacheByGet key cache
cacheBySet :: (MonadHandler m, Typeable a)
=> S.ByteString
-> a
-> m ()
cacheBySet key value = do
gs <- get
let cache = ghsCacheBy gs
newCache = Cache.cacheBySet key value cache
put $ gs { ghsCacheBy = newCache }
languages :: MonadHandler m => m [Text]
languages = do
mlang <- lookupSession langKey
langs <- reqLangs <$> getRequest
return $ maybe id (:) mlang langs
lookup' :: Eq a => a -> [(a, b)] -> [b]
lookup' a = map snd . filter (\x -> a == fst x)
lookupHeader :: MonadHandler m => CI S8.ByteString -> m (Maybe S8.ByteString)
lookupHeader = fmap listToMaybe . lookupHeaders
lookupHeaders :: MonadHandler m => CI S8.ByteString -> m [S8.ByteString]
lookupHeaders key = do
req <- waiRequest
return $ lookup' key $ W.requestHeaders req
lookupBasicAuth :: (MonadHandler m) => m (Maybe (Text, Text))
lookupBasicAuth = fmap (>>= getBA) (lookupHeader "Authorization")
where
getBA bs = (decodeUtf8With lenientDecode *** decodeUtf8With lenientDecode)
<$> extractBasicAuth bs
lookupBearerAuth :: (MonadHandler m) => m (Maybe Text)
lookupBearerAuth = fmap (>>= getBR)
(lookupHeader "Authorization")
where
getBR bs = decodeUtf8With lenientDecode
<$> extractBearerAuth bs
lookupGetParams :: MonadHandler m => Text -> m [Text]
lookupGetParams pn = do
rr <- getRequest
return $ lookup' pn $ reqGetParams rr
lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)
lookupGetParam = fmap listToMaybe . lookupGetParams
lookupPostParams :: (MonadResource m, MonadHandler m) => Text -> m [Text]
lookupPostParams pn = do
(pp, _) <- runRequestBody
return $ lookup' pn pp
lookupPostParam :: (MonadResource m, MonadHandler m)
=> Text
-> m (Maybe Text)
lookupPostParam = fmap listToMaybe . lookupPostParams
lookupFile :: MonadHandler m
=> Text
-> m (Maybe FileInfo)
lookupFile = fmap listToMaybe . lookupFiles
lookupFiles :: MonadHandler m
=> Text
-> m [FileInfo]
lookupFiles pn = do
(_, files) <- runRequestBody
return $ lookup' pn files
lookupCookie :: MonadHandler m => Text -> m (Maybe Text)
lookupCookie = fmap listToMaybe . lookupCookies
lookupCookies :: MonadHandler m => Text -> m [Text]
lookupCookies pn = do
rr <- getRequest
return $ lookup' pn $ reqCookies rr
selectRep :: MonadHandler m
=> Writer.Writer (Endo [ProvidedRep m]) ()
-> m TypedContent
selectRep w = do
cts <- fmap reqAccept getRequest
case mapMaybe tryAccept cts of
[] ->
case reps of
[] -> sendResponseStatus H.status500 ("No reps provided to selectRep" :: Text)
rep:_ -> returnRep rep
rep:_ -> returnRep rep
where
returnRep (ProvidedRep ct mcontent) = fmap (TypedContent ct) mcontent
reps = appEndo (Writer.execWriter w) []
repMap = Map.unions $ map (\v@(ProvidedRep k _) -> Map.fromList
[ (k, v)
, (noSpace k, v)
, (simpleContentType k, v)
]) reps
mainTypeMap = Map.fromList $ reverse $ map
(\v@(ProvidedRep ct _) -> (fst $ contentTypeTypes ct, v)) reps
tryAccept ct =
if subType == "*"
then if mainType == "*"
then listToMaybe reps
else Map.lookup mainType mainTypeMap
else lookupAccept ct
where
(mainType, subType) = contentTypeTypes ct
lookupAccept ct = Map.lookup ct repMap <|>
Map.lookup (noSpace ct) repMap <|>
Map.lookup (simpleContentType ct) repMap
noSpace = S8.filter (/= ' ')
data ProvidedRep m = ProvidedRep !ContentType !(m Content)
provideRep :: (Monad m, HasContentType a)
=> m a
-> Writer.Writer (Endo [ProvidedRep m]) ()
provideRep handler = provideRepType (getContentType handler) handler
provideRepType :: (Monad m, ToContent a)
=> ContentType
-> m a
-> Writer.Writer (Endo [ProvidedRep m]) ()
provideRepType ct handler =
Writer.tell $ Endo (ProvidedRep ct (liftM toContent handler):)
rawRequestBody :: MonadHandler m => ConduitT i S.ByteString m ()
rawRequestBody = do
req <- lift waiRequest
let loop = do
bs <- liftIO $ W.requestBody req
unless (S.null bs) $ do
yield bs
loop
loop
fileSource :: MonadResource m => FileInfo -> ConduitT () S.ByteString m ()
fileSource = transPipe liftResourceT . fileSourceRaw
fileSourceByteString :: MonadResource m => FileInfo -> m S.ByteString
fileSourceByteString fileInfo = runConduit (L.toStrict <$> (fileSource fileInfo .| sinkLazy))
respond :: (Monad m, ToContent a) => ContentType -> a -> m TypedContent
respond ct = return . TypedContent ct . toContent
respondSource :: ContentType
-> ConduitT () (Flush Builder) (HandlerFor site) ()
-> HandlerFor site TypedContent
respondSource ctype src = HandlerFor $ \hd ->
return $ TypedContent ctype $ ContentSource
$ transPipe (lift . flip unHandlerFor hd) src
sendChunk :: Monad m => ToFlushBuilder a => a -> ConduitT i (Flush Builder) m ()
sendChunk = yield . toFlushBuilder
sendFlush :: Monad m => ConduitT i (Flush Builder) m ()
sendFlush = yield Flush
sendChunkBS :: Monad m => S.ByteString -> ConduitT i (Flush Builder) m ()
sendChunkBS = sendChunk
sendChunkLBS :: Monad m => L.ByteString -> ConduitT i (Flush Builder) m ()
sendChunkLBS = sendChunk
sendChunkText :: Monad m => T.Text -> ConduitT i (Flush Builder) m ()
sendChunkText = sendChunk
sendChunkLazyText :: Monad m => TL.Text -> ConduitT i (Flush Builder) m ()
sendChunkLazyText = sendChunk
sendChunkHtml :: Monad m => Html -> ConduitT i (Flush Builder) m ()
sendChunkHtml = sendChunk
defaultCsrfCookieName :: S8.ByteString
defaultCsrfCookieName = "XSRF-TOKEN"
setCsrfCookie :: MonadHandler m => m ()
setCsrfCookie = setCsrfCookieWithCookie defaultSetCookie
{ setCookieName = defaultCsrfCookieName
, setCookiePath = Just "/"
}
setCsrfCookieWithCookie :: MonadHandler m => SetCookie -> m ()
setCsrfCookieWithCookie cookie = do
mCsrfToken <- reqToken <$> getRequest
Fold.forM_ mCsrfToken (\token -> setCookie $ cookie { setCookieValue = encodeUtf8 token })
defaultCsrfHeaderName :: CI S8.ByteString
defaultCsrfHeaderName = "X-XSRF-TOKEN"
checkCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m ()
checkCsrfHeaderNamed headerName = do
(valid, mHeader) <- hasValidCsrfHeaderNamed' headerName
unless valid (permissionDenied $ csrfErrorMessage [CSRFHeader (decodeUtf8 $ original headerName) mHeader])
hasValidCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m Bool
hasValidCsrfHeaderNamed headerName = fst <$> hasValidCsrfHeaderNamed' headerName
hasValidCsrfHeaderNamed' :: MonadHandler m => CI S8.ByteString -> m (Bool, Maybe Text)
hasValidCsrfHeaderNamed' headerName = do
mCsrfToken <- reqToken <$> getRequest
mXsrfHeader <- lookupHeader headerName
return $ (validCsrf mCsrfToken mXsrfHeader, decodeUtf8 <$> mXsrfHeader)
defaultCsrfParamName :: Text
defaultCsrfParamName = "_token"
checkCsrfParamNamed :: MonadHandler m => Text -> m ()
checkCsrfParamNamed paramName = do
(valid, mParam) <- hasValidCsrfParamNamed' paramName
unless valid (permissionDenied $ csrfErrorMessage [CSRFParam paramName mParam])
hasValidCsrfParamNamed :: MonadHandler m => Text -> m Bool
hasValidCsrfParamNamed paramName = fst <$> hasValidCsrfParamNamed' paramName
hasValidCsrfParamNamed' :: MonadHandler m => Text -> m (Bool, Maybe Text)
hasValidCsrfParamNamed' paramName = do
mCsrfToken <- reqToken <$> getRequest
mCsrfParam <- lookupPostParam paramName
return $ (validCsrf mCsrfToken (encodeUtf8 <$> mCsrfParam), mCsrfParam)
checkCsrfHeaderOrParam :: (MonadHandler m, MonadLogger m)
=> CI S8.ByteString
-> Text
-> m ()
checkCsrfHeaderOrParam headerName paramName = do
(validHeader, mHeader) <- hasValidCsrfHeaderNamed' headerName
(validParam, mParam) <- hasValidCsrfParamNamed' paramName
unless (validHeader || validParam) $ do
let errorMessage = csrfErrorMessage $ [CSRFHeader (decodeUtf8 $ original headerName) mHeader, CSRFParam paramName mParam]
$logWarnS "yesod-core" errorMessage
permissionDenied errorMessage
validCsrf :: Maybe Text -> Maybe S.ByteString -> Bool
validCsrf (Just token) (Just param) = encodeUtf8 token `constEq` param
validCsrf Nothing _param = True
validCsrf (Just _token) Nothing = False
data CSRFExpectation = CSRFHeader Text (Maybe Text)
| CSRFParam Text (Maybe Text)
csrfErrorMessage :: [CSRFExpectation]
-> Text
csrfErrorMessage expectedLocations = T.intercalate "\n"
[ "A valid CSRF token wasn't present. Because the request could have been forged, it's been rejected altogether."
, "If you're a developer of this site, these tips will help you debug the issue:"
, "- Read the Yesod.Core.Handler docs of the yesod-core package for details on CSRF protection."
, "- Check that your HTTP client is persisting cookies between requests, like a browser does."
, "- By default, the CSRF token is sent to the client in a cookie named " `mappend` (decodeUtf8 defaultCsrfCookieName) `mappend` "."
, "- The server is looking for the token in the following locations:\n" `mappend` T.intercalate "\n" (map csrfLocation expectedLocations)
]
where csrfLocation expected = case expected of
CSRFHeader k v -> T.intercalate " " [" - An HTTP header named", k, (formatValue v)]
CSRFParam k v -> T.intercalate " " [" - A POST parameter named", k, (formatValue v)]
formatValue :: Maybe Text -> Text
formatValue maybeText = case maybeText of
Nothing -> "(which is not currently set)"
Just t -> T.concat ["(which has the current, incorrect value: '", t, "')"]
getSubYesod :: MonadHandler m => m (SubHandlerSite m)
getSubYesod = liftSubHandler $ SubHandlerFor $ return . rheChild . handlerEnv
getRouteToParent :: MonadHandler m => m (Route (SubHandlerSite m) -> Route (HandlerSite m))
getRouteToParent = liftSubHandler $ SubHandlerFor $ return . rheRouteToMaster . handlerEnv
getSubCurrentRoute :: MonadHandler m => m (Maybe (Route (SubHandlerSite m)))
getSubCurrentRoute = liftSubHandler $ SubHandlerFor $ return . rheRoute . handlerEnv