module Yesod.Core.Class.Yesod where
import Control.Monad.Logger (logErrorS)
import Yesod.Core.Content
import Yesod.Core.Handler
import Yesod.Routes.Class
import Blaze.ByteString.Builder (Builder)
import Blaze.ByteString.Builder.Char.Utf8 (fromText)
import Control.Arrow ((***), second)
import Control.Exception (bracket)
import Control.Monad (forM, when, void)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther),
LogSource)
import Control.Monad.Trans.Resource (InternalState, createInternalState, closeInternalState)
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L
import Data.Aeson (object, (.=))
import Data.List (foldl')
import Data.List (nub)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Encoding.Error as TEE
import Data.Text.Lazy.Builder (toLazyText)
import Data.Text.Lazy.Encoding (encodeUtf8)
import Data.Word (Word64)
import Language.Haskell.TH.Syntax (Loc (..))
import Network.HTTP.Types (encodePath)
import qualified Network.Wai as W
import Data.Default (def)
import Network.Wai.Parse (lbsBackEnd,
tempFileBackEnd)
import Network.Wai.Logger (ZonedDate, clockDateCacher)
import System.Log.FastLogger
import Text.Blaze (customAttribute, textTag,
toValue, (!))
import Text.Blaze (preEscapedToMarkup)
import qualified Text.Blaze.Html5 as TBH
import Text.Hamlet
import Text.Julius
import qualified Web.ClientSession as CS
import Web.Cookie (parseCookies)
import Web.Cookie (SetCookie (..))
import Yesod.Core.Types
import Yesod.Core.Internal.Session
import Yesod.Core.Widget
import Control.Monad.Trans.Class (lift)
class RenderRoute site => Yesod site where
approot :: Approot site
approot = ApprootRelative
errorHandler :: ErrorResponse -> HandlerT site IO TypedContent
errorHandler = defaultErrorHandler
defaultLayout :: WidgetT site IO () -> HandlerT site IO Html
defaultLayout w = do
p <- widgetToPageContent w
mmsg <- getMessage
withUrlRenderer [hamlet|
$newline never
$doctype 5
<html>
<head>
<title>#{pageTitle p}
^{pageHead p}
<body>
$maybe msg <- mmsg
<p .message>#{msg}
^{pageBody p}
|]
urlRenderOverride :: site -> Route site -> Maybe Builder
urlRenderOverride _ _ = Nothing
isAuthorized :: Route site
-> Bool
-> HandlerT site IO AuthResult
isAuthorized _ _ = return Authorized
isWriteRequest :: Route site -> HandlerT site IO Bool
isWriteRequest _ = do
wai <- waiRequest
return $ W.requestMethod wai `notElem`
["GET", "HEAD", "OPTIONS", "TRACE"]
authRoute :: site -> Maybe (Route site)
authRoute _ = Nothing
cleanPath :: site -> [Text] -> Either [Text] [Text]
cleanPath _ s =
if corrected == s
then Right $ map dropDash s
else Left corrected
where
corrected = filter (not . T.null) s
dropDash t
| T.all (== '-') t = T.drop 1 t
| otherwise = t
joinPath :: site
-> T.Text
-> [T.Text]
-> [(T.Text, T.Text)]
-> Builder
joinPath _ ar pieces' qs' =
fromText ar `mappend` encodePath pieces qs
where
pieces = if null pieces' then [""] else map addDash pieces'
qs = map (TE.encodeUtf8 *** go) qs'
go "" = Nothing
go x = Just $ TE.encodeUtf8 x
addDash t
| T.all (== '-') t = T.cons '-' t
| otherwise = t
addStaticContent :: Text
-> Text
-> L.ByteString
-> HandlerT site IO (Maybe (Either Text (Route site, [(Text, Text)])))
addStaticContent _ _ _ = return Nothing
maximumContentLength :: site -> Maybe (Route site) -> Maybe Word64
maximumContentLength _ _ = Just $ 2 * 1024 * 1024
makeLogger :: site -> IO Logger
makeLogger _ = do
loggerSet' <- newStdoutLoggerSet defaultBufSize
(getter, _) <- clockDateCacher
return $! Logger loggerSet' getter
messageLoggerSource :: site
-> Logger
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
messageLoggerSource a logger loc source level msg = do
sl <- shouldLogIO a source level
when sl $
formatLogMessage (loggerDate logger) loc source level msg >>= loggerPutStr logger
jsLoader :: site -> ScriptLoadPosition site
jsLoader _ = BottomOfBody
makeSessionBackend :: site -> IO (Maybe SessionBackend)
makeSessionBackend _ = fmap Just $ defaultClientSessionBackend 120 CS.defaultKeyFile
fileUpload :: site -> W.RequestBodyLength -> FileUpload
fileUpload _ (W.KnownLength size)
| size <= 50000 = FileUploadMemory lbsBackEnd
fileUpload _ _ = FileUploadDisk tempFileBackEnd
shouldLog :: site -> LogSource -> LogLevel -> Bool
shouldLog _ _ level = level >= LevelInfo
shouldLogIO :: site -> LogSource -> LogLevel -> IO Bool
shouldLogIO a b c = return (shouldLog a b c)
yesodMiddleware :: ToTypedContent res => HandlerT site IO res -> HandlerT site IO res
yesodMiddleware = defaultYesodMiddleware
yesodWithInternalState :: site -> Maybe (Route site) -> (InternalState -> IO a) -> IO a
yesodWithInternalState _ _ = bracket createInternalState closeInternalState
defaultYesodMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res
defaultYesodMiddleware handler = do
addHeader "Vary" "Accept, Accept-Language"
authorizationCheck
handler
sslOnlySessions :: IO (Maybe SessionBackend) -> IO (Maybe SessionBackend)
sslOnlySessions = (fmap . fmap) secureSessionCookies
where
setSecureBit cookie = cookie { setCookieSecure = True }
secureSessionCookies = customizeSessionCookies setSecureBit
sslOnlyMiddleware :: Yesod site
=> Int
-> HandlerT site IO res
-> HandlerT site IO res
sslOnlyMiddleware timeout handler = do
addHeader "Strict-Transport-Security"
$ T.pack $ concat [ "max-age="
, show $ timeout * 60
, "; includeSubDomains"
]
handler
authorizationCheck :: Yesod site => HandlerT site IO ()
authorizationCheck = do
getCurrentRoute >>= maybe (return ()) checkUrl
where
checkUrl url = do
isWrite <- isWriteRequest url
ar <- isAuthorized url isWrite
case ar of
Authorized -> return ()
AuthenticationRequired -> do
master <- getYesod
case authRoute master of
Nothing -> void $ notAuthenticated
Just url' -> do
void $ selectRep $ do
provideRepType typeHtml $ do
setUltDestCurrent
void $ redirect url'
provideRepType typeJson $
void $ notAuthenticated
Unauthorized s' -> permissionDenied s'
widgetToPageContent :: (Eq (Route site), Yesod site)
=> WidgetT site IO ()
-> HandlerT site IO (PageContent (Route site))
widgetToPageContent w = do
master <- getYesod
hd <- HandlerT return
((), GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- lift $ unWidgetT w hd
let title = maybe mempty unTitle mTitle
scripts = runUniqueList scripts'
stylesheets = runUniqueList stylesheets'
render <- getUrlRenderParams
let renderLoc x =
case x of
Nothing -> Nothing
Just (Left s) -> Just s
Just (Right (u, p)) -> Just $ render u p
css <- forM (Map.toList style) $ \(mmedia, content) -> do
let rendered = toLazyText $ content render
x <- addStaticContent "css" "text/css; charset=utf-8"
$ encodeUtf8 rendered
return (mmedia,
case x of
Nothing -> Left $ preEscapedToMarkup rendered
Just y -> Right $ either id (uncurry render) y)
jsLoc <-
case jscript of
Nothing -> return Nothing
Just s -> do
x <- addStaticContent "js" "text/javascript; charset=utf-8"
$ encodeUtf8 $ renderJavascriptUrl render s
return $ renderLoc x
let (mcomplete, asyncScripts) = asyncHelper render scripts jscript jsLoc
regularScriptLoad = [hamlet|
$newline never
$forall s <- scripts
^{mkScriptTag s}
$maybe j <- jscript
$maybe s <- jsLoc
<script src="#{s}">
$nothing
<script>^{jelper j}
|]
headAll = [hamlet|
$newline never
\^{head'}
$forall s <- stylesheets
^{mkLinkTag s}
$forall s <- css
$maybe t <- right $ snd s
$maybe media <- fst s
<link rel=stylesheet media=#{media} href=#{t}>
$nothing
<link rel=stylesheet href=#{t}>
$maybe content <- left $ snd s
$maybe media <- fst s
<style media=#{media}>#{content}
$nothing
<style>#{content}
$case jsLoader master
$of BottomOfBody
$of BottomOfHeadAsync asyncJsLoader
^{asyncJsLoader asyncScripts mcomplete}
$of BottomOfHeadBlocking
^{regularScriptLoad}
|]
let bodyScript = [hamlet|
$newline never
^{body}
^{regularScriptLoad}
|]
return $ PageContent title headAll $
case jsLoader master of
BottomOfBody -> bodyScript
_ -> body
where
renderLoc' render' (Local url) = render' url []
renderLoc' _ (Remote s) = s
addAttr x (y, z) = x ! customAttribute (textTag y) (toValue z)
mkScriptTag (Script loc attrs) render' =
foldl' addAttr TBH.script (("src", renderLoc' render' loc) : attrs) $ return ()
mkLinkTag (Stylesheet loc attrs) render' =
foldl' addAttr TBH.link
( ("rel", "stylesheet")
: ("href", renderLoc' render' loc)
: attrs
)
runUniqueList :: Eq x => UniqueList x -> [x]
runUniqueList (UniqueList x) = nub $ x []
defaultErrorHandler :: Yesod site => ErrorResponse -> HandlerT site IO TypedContent
defaultErrorHandler NotFound = selectRep $ do
provideRep $ defaultLayout $ do
r <- waiRequest
let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r
setTitle "Not Found"
toWidget [hamlet|
<h1>Not Found
<p>#{path'}
|]
provideRep $ return $ object ["message" .= ("Not Found" :: Text)]
defaultErrorHandler NotAuthenticated = selectRep $ do
provideRep $ defaultLayout $ do
setTitle "Not logged in"
toWidget [hamlet|
<h1>Not logged in
<p style="display:none;">Set the authRoute and the user will be redirected there.
|]
provideRep $ do
addHeader "WWW-Authenticate" "RedirectJSON realm=\"application\", param=\"authentication_url\""
site <- getYesod
rend <- getUrlRender
return $ object $ [
"message" .= ("Not logged in"::Text)
] ++
case authRoute site of
Nothing -> []
Just url -> ["authentication_url" .= rend url]
defaultErrorHandler (PermissionDenied msg) = selectRep $ do
provideRep $ defaultLayout $ do
setTitle "Permission Denied"
toWidget [hamlet|
<h1>Permission denied
<p>#{msg}
|]
provideRep $
return $ object $ [
"message" .= ("Permission Denied. " <> msg)
]
defaultErrorHandler (InvalidArgs ia) = selectRep $ do
provideRep $ defaultLayout $ do
setTitle "Invalid Arguments"
toWidget [hamlet|
<h1>Invalid Arguments
<ul>
$forall msg <- ia
<li>#{msg}
|]
provideRep $ return $ object ["message" .= ("Invalid Arguments" :: Text), "errors" .= ia]
defaultErrorHandler (InternalError e) = do
$logErrorS "yesod-core" e
selectRep $ do
provideRep $ defaultLayout $ do
setTitle "Internal Server Error"
toWidget [hamlet|
<h1>Internal Server Error
<pre>#{e}
|]
provideRep $ return $ object ["message" .= ("Internal Server Error" :: Text), "error" .= e]
defaultErrorHandler (BadMethod m) = selectRep $ do
provideRep $ defaultLayout $ do
setTitle"Bad Method"
toWidget [hamlet|
<h1>Method Not Supported
<p>Method <code>#{S8.unpack m}</code> not supported
|]
provideRep $ return $ object ["message" .= ("Bad method" :: Text), "method" .= TE.decodeUtf8With TEE.lenientDecode m]
asyncHelper :: (url -> [x] -> Text)
-> [Script (url)]
-> Maybe (JavascriptUrl (url))
-> Maybe Text
-> (Maybe (HtmlUrl url), [Text])
asyncHelper render scripts jscript jsLoc =
(mcomplete, scripts'')
where
scripts' = map goScript scripts
scripts'' =
case jsLoc of
Just s -> scripts' ++ [s]
Nothing -> scripts'
goScript (Script (Local url) _) = render url []
goScript (Script (Remote s) _) = s
mcomplete =
case jsLoc of
Just{} -> Nothing
Nothing ->
case jscript of
Nothing -> Nothing
Just j -> Just $ jelper j
formatLogMessage :: IO ZonedDate
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO LogStr
formatLogMessage getdate loc src level msg = do
now <- getdate
return $
toLogStr now `mappend`
" [" `mappend`
(case level of
LevelOther t -> toLogStr t
_ -> toLogStr $ drop 5 $ show level) `mappend`
(if T.null src
then mempty
else "#" `mappend` toLogStr src) `mappend`
"] " `mappend`
msg `mappend`
" @(" `mappend`
toLogStr (fileLocationToString loc) `mappend`
")\n"
customizeSessionCookies :: (SetCookie -> SetCookie) -> (SessionBackend -> SessionBackend)
customizeSessionCookies customizeCookie backend = backend'
where
customizeHeader (AddCookie cookie) = AddCookie (customizeCookie cookie)
customizeHeader other = other
customizeSaveSession = (fmap . fmap . fmap) customizeHeader
backend' =
backend {
sbLoadSession = \req ->
second customizeSaveSession `fmap` sbLoadSession backend req
}
defaultClientSessionBackend :: Int
-> FilePath
-> IO SessionBackend
defaultClientSessionBackend minutes fp = do
key <- CS.getKey fp
let timeout = fromIntegral (minutes * 60)
(getCachedDate, _closeDateCacher) <- clientSessionDateCacher timeout
return $ clientSessionBackend key getCachedDate
envClientSessionBackend :: Int
-> String
-> IO SessionBackend
envClientSessionBackend minutes name = do
key <- CS.getKeyEnv name
let timeout = fromIntegral (minutes * 60)
(getCachedDate, _closeDateCacher) <- clientSessionDateCacher timeout
return $ clientSessionBackend key getCachedDate
jsToHtml :: Javascript -> Html
jsToHtml (Javascript b) = preEscapedToMarkup $ toLazyText b
jelper :: JavascriptUrl url -> HtmlUrl url
jelper = fmap jsToHtml
left :: Either a b -> Maybe a
left (Left x) = Just x
left _ = Nothing
right :: Either a b -> Maybe b
right (Right x) = Just x
right _ = Nothing
clientSessionBackend :: CS.Key
-> IO ClientSessionDateCache
-> SessionBackend
clientSessionBackend key getCachedDate =
SessionBackend {
sbLoadSession = loadClientSession key getCachedDate "_SESSION"
}
loadClientSession :: CS.Key
-> IO ClientSessionDateCache
-> S8.ByteString
-> W.Request
-> IO (SessionMap, SaveSession)
loadClientSession key getCachedDate sessionName req = load
where
load = do
date <- getCachedDate
return (sess date, save date)
sess date = fromMaybe Map.empty $ do
raw <- lookup "Cookie" $ W.requestHeaders req
val <- lookup sessionName $ parseCookies raw
let host = ""
decodeClientSession key date host val
save date sess' = do
iv <- liftIO CS.randomIV
return [AddCookie def
{ setCookieName = sessionName
, setCookieValue = encodeClientSession key iv date host sess'
, setCookiePath = Just "/"
, setCookieExpires = Just (csdcExpires date)
, setCookieDomain = Nothing
, setCookieHttpOnly = True
}]
where
host = ""
fileLocationToString :: Loc -> String
fileLocationToString loc = (loc_package loc) ++ ':' : (loc_module loc) ++
' ' : (loc_filename loc) ++ ':' : (line loc) ++ ':' : (char loc)
where
line = show . fst . loc_start
char = show . snd . loc_start