" res
diff --git a/yesod-core/Yesod/Content.hs b/yesod-core/Yesod/Content.hs
new file mode 100644
index 00000000..6244474c
--- /dev/null
+++ b/yesod-core/Yesod/Content.hs
@@ -0,0 +1,232 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE Rank2Types #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Yesod.Content
+ ( -- * Content
+ Content (..)
+ , emptyContent
+ , ToContent (..)
+ -- * Mime types
+ -- ** Data type
+ , ContentType
+ , typeHtml
+ , typePlain
+ , typeJson
+ , typeXml
+ , typeAtom
+ , typeRss
+ , typeJpeg
+ , typePng
+ , typeGif
+ , typeJavascript
+ , typeCss
+ , typeFlv
+ , typeOgv
+ , typeOctet
+ -- * Utilities
+ , simpleContentType
+ -- * Representations
+ , ChooseRep
+ , HasReps (..)
+ , defChooseRep
+ -- ** Specific content types
+ , RepHtml (..)
+ , RepJson (..)
+ , RepHtmlJson (..)
+ , RepPlain (..)
+ , RepXml (..)
+ -- * Utilities
+ , formatW3
+ , formatRFC1123
+ , formatRFC822
+ ) where
+
+import Data.Maybe (mapMaybe)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as L
+import Data.Text.Lazy (Text, pack)
+import qualified Data.Text as T
+
+import Data.Time
+import System.Locale
+
+import qualified Data.Text.Encoding
+import qualified Data.Text.Lazy.Encoding
+
+import Data.Enumerator (Enumerator)
+import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString)
+import Data.Monoid (mempty)
+
+import Text.Hamlet (Html)
+import Text.Blaze.Renderer.Utf8 (renderHtmlBuilder)
+import Data.String (IsString (fromString))
+import Network.Wai (FilePart)
+
+data Content = ContentBuilder Builder (Maybe Int) -- ^ The content and optional content length.
+ | ContentEnum (forall a. Enumerator Builder IO a)
+ | ContentFile FilePath (Maybe FilePart)
+
+-- | Zero-length enumerator.
+emptyContent :: Content
+emptyContent = ContentBuilder mempty $ Just 0
+
+instance IsString Content where
+ fromString = toContent
+
+-- | Anything which can be converted into 'Content'. Most of the time, you will
+-- want to use the 'ContentBuilder' constructor. An easier approach will be to use
+-- a pre-defined 'toContent' function, such as converting your data into a lazy
+-- bytestring and then calling 'toContent' on that.
+--
+-- Please note that the built-in instances for lazy data structures ('String',
+-- lazy 'L.ByteString', lazy 'Text' and 'Html') will not automatically include
+-- the content length for the 'ContentBuilder' constructor.
+class ToContent a where
+ toContent :: a -> Content
+
+instance ToContent Builder where
+ toContent = flip ContentBuilder Nothing
+instance ToContent B.ByteString where
+ toContent bs = ContentBuilder (fromByteString bs) $ Just $ B.length bs
+instance ToContent L.ByteString where
+ toContent = flip ContentBuilder Nothing . fromLazyByteString
+instance ToContent T.Text where
+ toContent = toContent . Data.Text.Encoding.encodeUtf8
+instance ToContent Text where
+ toContent = toContent . Data.Text.Lazy.Encoding.encodeUtf8
+instance ToContent String where
+ toContent = toContent . pack
+instance ToContent Html where
+ toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing
+
+-- | A function which gives targetted representations of content based on the
+-- content-types the user accepts.
+type ChooseRep =
+ [ContentType] -- ^ list of content-types user accepts, ordered by preference
+ -> IO (ContentType, Content)
+
+-- | Any type which can be converted to representations.
+class HasReps a where
+ chooseRep :: a -> ChooseRep
+
+-- | A helper method for generating 'HasReps' instances.
+--
+-- This function should be given a list of pairs of content type and conversion
+-- functions. If none of the content types match, the first pair is used.
+defChooseRep :: [(ContentType, a -> IO Content)] -> a -> ChooseRep
+defChooseRep reps a ts = do
+ let (ct, c) =
+ case mapMaybe helper ts of
+ (x:_) -> x
+ [] -> case reps of
+ [] -> error "Empty reps to defChooseRep"
+ (x:_) -> x
+ c' <- c a
+ return (ct, c')
+ where
+ helper ct = do
+ c <- lookup ct reps
+ return (ct, c)
+
+instance HasReps ChooseRep where
+ chooseRep = id
+
+instance HasReps () where
+ chooseRep = defChooseRep [(typePlain, const $ return $ toContent B.empty)]
+
+instance HasReps (ContentType, Content) where
+ chooseRep = const . return
+
+instance HasReps [(ContentType, Content)] where
+ chooseRep a cts = return $
+ case filter (\(ct, _) -> go ct `elem` map go cts) a of
+ ((ct, c):_) -> (ct, c)
+ _ -> case a of
+ (x:_) -> x
+ _ -> error "chooseRep [(ContentType, Content)] of empty"
+ where
+ go = simpleContentType
+
+newtype RepHtml = RepHtml Content
+instance HasReps RepHtml where
+ chooseRep (RepHtml c) _ = return (typeHtml, c)
+newtype RepJson = RepJson Content
+instance HasReps RepJson where
+ chooseRep (RepJson c) _ = return (typeJson, c)
+data RepHtmlJson = RepHtmlJson Content Content
+instance HasReps RepHtmlJson where
+ chooseRep (RepHtmlJson html json) = chooseRep
+ [ (typeHtml, html)
+ , (typeJson, json)
+ ]
+newtype RepPlain = RepPlain Content
+instance HasReps RepPlain where
+ chooseRep (RepPlain c) _ = return (typePlain, c)
+newtype RepXml = RepXml Content
+instance HasReps RepXml where
+ chooseRep (RepXml c) _ = return (typeXml, c)
+
+type ContentType = B.ByteString -- FIXME Text?
+
+typeHtml :: ContentType
+typeHtml = "text/html; charset=utf-8"
+
+typePlain :: ContentType
+typePlain = "text/plain; charset=utf-8"
+
+typeJson :: ContentType
+typeJson = "application/json; charset=utf-8"
+
+typeXml :: ContentType
+typeXml = "text/xml"
+
+typeAtom :: ContentType
+typeAtom = "application/atom+xml"
+
+typeRss :: ContentType
+typeRss = "application/rss+xml"
+
+typeJpeg :: ContentType
+typeJpeg = "image/jpeg"
+
+typePng :: ContentType
+typePng = "image/png"
+
+typeGif :: ContentType
+typeGif = "image/gif"
+
+typeJavascript :: ContentType
+typeJavascript = "text/javascript; charset=utf-8"
+
+typeCss :: ContentType
+typeCss = "text/css; charset=utf-8"
+
+typeFlv :: ContentType
+typeFlv = "video/x-flv"
+
+typeOgv :: ContentType
+typeOgv = "video/ogg"
+
+typeOctet :: ContentType
+typeOctet = "application/octet-stream"
+
+-- | Removes \"extra\" information at the end of a content type string. In
+-- particular, removes everything after the semicolon, if present.
+--
+-- For example, \"text/html; charset=utf-8\" is commonly used to specify the
+-- character encoding for HTML data. This function would return \"text/html\".
+simpleContentType :: ContentType -> ContentType
+simpleContentType = fst . B.breakByte 59 -- 59 == ;
+
+-- | Format a 'UTCTime' in W3 format.
+formatW3 :: UTCTime -> T.Text
+formatW3 = T.pack . formatTime defaultTimeLocale "%FT%X-00:00"
+
+-- | Format as per RFC 1123.
+formatRFC1123 :: UTCTime -> T.Text
+formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
+
+-- | Format as per RFC 822.
+formatRFC822 :: UTCTime -> T.Text
+formatRFC822 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S %z"
diff --git a/yesod-core/Yesod/Core.hs b/yesod-core/Yesod/Core.hs
new file mode 100644
index 00000000..67ed22e0
--- /dev/null
+++ b/yesod-core/Yesod/Core.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Yesod.Core
+ ( -- * Type classes
+ Yesod (..)
+ , YesodDispatch (..)
+ , RenderRoute (..)
+ -- ** Breadcrumbs
+ , YesodBreadcrumbs (..)
+ , breadcrumbs
+ -- * Utitlities
+ , maybeAuthorized
+ , widgetToPageContent
+ -- * Defaults
+ , defaultErrorHandler
+ -- * Data types
+ , AuthResult (..)
+ -- * Logging
+ , LogLevel (..)
+ , formatLogMessage
+ , logDebug
+ , logInfo
+ , logWarn
+ , logError
+ , logOther
+ -- * Misc
+ , yesodVersion
+ , yesodRender
+ -- * Re-exports
+ , module Yesod.Content
+ , module Yesod.Dispatch
+ , module Yesod.Handler
+ , module Yesod.Request
+ , module Yesod.Widget
+ , module Yesod.Message
+ ) where
+
+import Yesod.Internal.Core
+import Yesod.Content
+import Yesod.Dispatch
+import Yesod.Handler
+import Yesod.Request
+import Yesod.Widget
+import Yesod.Message
+
+import Language.Haskell.TH.Syntax
+import Data.Text (Text)
+
+logTH :: LogLevel -> Q Exp
+logTH level =
+ [|messageLoggerHandler $(qLocation >>= liftLoc) $(lift level)|]
+ where
+ liftLoc :: Loc -> Q Exp
+ liftLoc (Loc a b c d e) = [|Loc $(lift a) $(lift b) $(lift c) $(lift d) $(lift e)|]
+
+-- | Generates a function that takes a 'Text' and logs a 'LevelDebug' message. Usage:
+--
+-- > $(logDebug) "This is a debug log message"
+logDebug :: Q Exp
+logDebug = logTH LevelDebug
+
+-- | See 'logDebug'
+logInfo :: Q Exp
+logInfo = logTH LevelInfo
+-- | See 'logDebug'
+logWarn :: Q Exp
+logWarn = logTH LevelWarn
+-- | See 'logDebug'
+logError :: Q Exp
+logError = logTH LevelError
+
+-- | Generates a function that takes a 'Text' and logs a 'LevelOther' message. Usage:
+--
+-- > $(logOther "My new level") "This is a log message"
+logOther :: Text -> Q Exp
+logOther = logTH . LevelOther
diff --git a/yesod-core/Yesod/Dispatch.hs b/yesod-core/Yesod/Dispatch.hs
new file mode 100644
index 00000000..de219d05
--- /dev/null
+++ b/yesod-core/Yesod/Dispatch.hs
@@ -0,0 +1,180 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Yesod.Dispatch
+ ( -- * Quasi-quoted routing
+ parseRoutes
+ , parseRoutesFile
+ , mkYesod
+ , mkYesodSub
+ -- ** More fine-grained
+ , mkYesodData
+ , mkYesodSubData
+ , mkYesodDispatch
+ , mkYesodSubDispatch
+ -- ** Path pieces
+ , SinglePiece (..)
+ , MultiPiece (..)
+ , Texts
+ -- * Convert to WAI
+ , toWaiApp
+ , toWaiAppPlain
+ ) where
+
+import Data.Functor ((<$>))
+import Data.Either (partitionEithers)
+import Prelude hiding (exp)
+import Yesod.Internal.Core
+import Yesod.Handler
+import Yesod.Internal.Dispatch
+
+import Web.PathPieces (SinglePiece (..), MultiPiece (..))
+import Yesod.Internal.RouteParsing (THResource, Pieces (..), createRoutes, createRender, Resource (..), parseRoutes, parseRoutesFile)
+import Language.Haskell.TH.Syntax
+
+import qualified Network.Wai as W
+import Network.Wai.Middleware.Jsonp
+import Network.Wai.Middleware.Gzip
+import Network.Wai.Middleware.Autohead
+
+import Data.ByteString.Lazy.Char8 ()
+
+import Web.ClientSession
+import Data.Char (isUpper)
+import Data.Text (Text)
+
+type Texts = [Text]
+
+-- | Generates URL datatype and site function for the given 'Resource's. This
+-- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
+-- Use 'parseRoutes' to create the 'Resource's.
+mkYesod :: String -- ^ name of the argument datatype
+ -> [Resource]
+ -> Q [Dec]
+mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
+
+-- | Generates URL datatype and site function for the given 'Resource's. This
+-- is used for creating subsites, /not/ sites. See 'mkYesod' for the latter.
+-- Use 'parseRoutes' to create the 'Resource's. In general, a subsite is not
+-- executable by itself, but instead provides functionality to
+-- be embedded in other sites.
+mkYesodSub :: String -- ^ name of the argument datatype
+ -> Cxt
+ -> [Resource]
+ -> Q [Dec]
+mkYesodSub name clazzes =
+ fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True
+ where
+ (name':rest) = words name
+
+-- | Sometimes, you will want to declare your routes in one file and define
+-- your handlers elsewhere. For example, this is the only way to break up a
+-- monolithic file into smaller parts. Use this function, paired with
+-- 'mkYesodDispatch', to do just that.
+mkYesodData :: String -> [Resource] -> Q [Dec]
+mkYesodData name res = mkYesodDataGeneral name [] False res
+
+mkYesodSubData :: String -> Cxt -> [Resource] -> Q [Dec]
+mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res
+
+mkYesodDataGeneral :: String -> Cxt -> Bool -> [Resource] -> Q [Dec]
+mkYesodDataGeneral name clazzes isSub res = do
+ let (name':rest) = words name
+ (x, _) <- mkYesodGeneral name' rest clazzes isSub res
+ let rname = mkName $ "resources" ++ name
+ eres <- lift res
+ let y = [ SigD rname $ ListT `AppT` ConT ''Resource
+ , FunD rname [Clause [] (NormalB eres) []]
+ ]
+ return $ x ++ y
+
+-- | See 'mkYesodData'.
+mkYesodDispatch :: String -> [Resource] -> Q [Dec]
+mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False
+
+mkYesodSubDispatch :: String -> Cxt -> [Resource] -> Q [Dec]
+mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True
+ where (name':rest) = words name
+
+mkYesodGeneral :: String -- ^ foundation name
+ -> [String] -- ^ parameters for foundation
+ -> Cxt -- ^ classes
+ -> Bool -- ^ is subsite?
+ -> [Resource]
+ -> Q ([Dec], [Dec])
+mkYesodGeneral name args clazzes isSub res = do
+ let name' = mkName name
+ args' = map mkName args
+ arg = foldl AppT (ConT name') $ map VarT args'
+ th' <- mapM thResourceFromResource res
+ let th = map fst th'
+ w' <- createRoutes th
+ let routesName = mkName $ name ++ "Route"
+ let w = DataD [] routesName [] w' [''Show, ''Read, ''Eq]
+ let x = TySynInstD ''Route [arg] $ ConT routesName
+
+ render <- createRender th
+ let x' = InstanceD [] (ConT ''RenderRoute `AppT` ConT routesName)
+ [ FunD (mkName "renderRoute") render
+ ]
+
+ let splitter :: (THResource, Maybe String)
+ -> Either
+ (THResource, Maybe String)
+ (THResource, Maybe String)
+ splitter a@((_, SubSite{}), _) = Left a
+ splitter a = Right a
+ let (resSub, resLoc) = partitionEithers $ map splitter th'
+ yd <- mkYesodDispatch' resSub resLoc
+ let master = mkName "master"
+ let ctx = if isSub
+ then ClassP (mkName "Yesod") [VarT master] : clazzes
+ else []
+ let ytyp = if isSub
+ then ConT ''YesodDispatch `AppT` arg `AppT` VarT master
+ else ConT ''YesodDispatch `AppT` arg `AppT` arg
+ let y = InstanceD ctx ytyp [FunD (mkName "yesodDispatch") [yd]]
+ return ([w, x, x'], [y])
+
+thResourceFromResource :: Resource -> Q (THResource, Maybe String)
+thResourceFromResource (Resource n ps atts)
+ | all (all isUpper) atts = return ((n, Simple ps atts), Nothing)
+thResourceFromResource (Resource n ps [stype, toSubArg]) = do
+ let stype' = ConT $ mkName stype
+ parse <- [|error "ssParse"|]
+ dispatch <- [|error "ssDispatch"|]
+ render <- [|renderRoute|]
+ tmg <- [|error "ssToMasterArg"|]
+ return ((n, SubSite
+ { ssType = ConT ''Route `AppT` stype'
+ , ssParse = parse
+ , ssRender = render
+ , ssDispatch = dispatch
+ , ssToMasterArg = tmg
+ , ssPieces = ps
+ }), Just toSubArg)
+
+thResourceFromResource (Resource n _ _) =
+ error $ "Invalid attributes for resource: " ++ n
+
+-- | Convert the given argument into a WAI application, executable with any WAI
+-- handler. This is the same as 'toWaiAppPlain', except it includes three
+-- middlewares: GZIP compression, JSON-P and path cleaning. This is the
+-- recommended approach for most users.
+toWaiApp :: (Yesod y, YesodDispatch y y) => y -> IO W.Application
+toWaiApp y = gzip False . jsonp . autohead <$> toWaiAppPlain y
+
+-- | Convert the given argument into a WAI application, executable with any WAI
+-- handler. This differs from 'toWaiApp' in that it uses no middlewares.
+toWaiAppPlain :: (Yesod y, YesodDispatch y y) => y -> IO W.Application
+toWaiAppPlain a = toWaiApp' a <$> encryptKey a
+
+
+toWaiApp' :: (Yesod y, YesodDispatch y y)
+ => y
+ -> Maybe Key
+ -> W.Application
+toWaiApp' y key' env =
+ case yesodDispatch y key' (W.pathInfo env) y id of
+ Just app -> app env
+ Nothing -> yesodRunner y y id key' Nothing notFound env
diff --git a/yesod-core/Yesod/Handler.hs b/yesod-core/Yesod/Handler.hs
new file mode 100644
index 00000000..34cfcc51
--- /dev/null
+++ b/yesod-core/Yesod/Handler.hs
@@ -0,0 +1,904 @@
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+---------------------------------------------------------
+--
+-- Module : Yesod.Handler
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : unstable
+-- Portability : portable
+--
+-- Define Handler stuff.
+--
+---------------------------------------------------------
+module Yesod.Handler
+ ( -- * Type families
+ Route
+ , YesodSubRoute (..)
+ -- * Handler monad
+ , GHandler
+ , GGHandler
+ -- ** Read information from handler
+ , getYesod
+ , getYesodSub
+ , getUrlRender
+ , getUrlRenderParams
+ , getCurrentRoute
+ , getRouteToMaster
+ , getRequest
+ , waiRequest
+ , runRequestBody
+ -- * Special responses
+ -- ** Redirecting
+ , RedirectType (..)
+ , redirect
+ , redirectParams
+ , redirectString
+ , redirectText
+ , redirectToPost
+ -- ** Errors
+ , notFound
+ , badMethod
+ , permissionDenied
+ , permissionDeniedI
+ , invalidArgs
+ , invalidArgsI
+ -- ** Short-circuit responses.
+ , sendFile
+ , sendFilePart
+ , sendResponse
+ , sendResponseStatus
+ , sendResponseCreated
+ , sendWaiResponse
+ -- * Setting headers
+ , setCookie
+ , deleteCookie
+ , setHeader
+ , setLanguage
+ -- ** Content caching and expiration
+ , cacheSeconds
+ , neverExpires
+ , alreadyExpired
+ , expiresAt
+ -- * Session
+ , SessionMap
+ , lookupSession
+ , getSession
+ , setSession
+ , deleteSession
+ -- ** Ultimate destination
+ , setUltDest
+ , setUltDestString
+ , setUltDestText
+ , setUltDest'
+ , setUltDestReferer
+ , redirectUltDest
+ , clearUltDest
+ -- ** Messages
+ , setMessage
+ , setMessageI
+ , getMessage
+ -- * Helpers for specific content
+ -- ** Hamlet
+ , hamletToContent
+ , hamletToRepHtml
+ -- ** Misc
+ , newIdent
+ , liftIOHandler
+ -- * i18n
+ , getMessageRender
+ -- * Internal Yesod
+ , runHandler
+ , YesodApp (..)
+ , runSubsiteGetter
+ , toMasterHandler
+ , toMasterHandlerDyn
+ , toMasterHandlerMaybe
+ , localNoCurrent
+ , HandlerData
+ , ErrorResponse (..)
+ , YesodAppResult (..)
+ , handlerToYAR
+ , yarToResponse
+ , headerToPair
+ ) where
+
+import Prelude hiding (catch)
+import Yesod.Internal.Request
+import Yesod.Internal
+import Data.Time (UTCTime)
+
+import Control.Exception hiding (Handler, catch, finally)
+import qualified Control.Exception as E
+import Control.Applicative
+
+import Control.Monad (liftM, join, MonadPlus)
+
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Class
+import Control.Monad.Trans.Writer
+import Control.Monad.Trans.Reader
+import Control.Monad.Trans.State
+import Control.Monad.Trans.Error (throwError, ErrorT (..), Error (..))
+
+import System.IO
+import qualified Network.Wai as W
+import qualified Network.HTTP.Types as H
+import Control.Failure (Failure (failure))
+
+import Text.Hamlet
+import qualified Text.Blaze.Renderer.Text
+import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
+import Data.Text.Encoding.Error (lenientDecode)
+import qualified Data.Text.Lazy as TL
+
+import Control.Monad.IO.Control (MonadControlIO)
+import Control.Monad.Trans.Control (MonadTransControl, liftControl)
+import qualified Data.Map as Map
+import qualified Data.ByteString as S
+import Data.ByteString (ByteString)
+import Data.Enumerator (Iteratee (..), run_, ($$))
+import Network.Wai.Parse (parseHttpAccept)
+
+import Yesod.Content
+import Data.Maybe (fromMaybe)
+import Web.Cookie (SetCookie (..), renderSetCookie)
+import Control.Arrow (second, (***))
+import qualified Network.Wai.Parse as NWP
+import Data.Monoid (mappend, mempty, Endo (..))
+import qualified Data.ByteString.Char8 as S8
+import Data.CaseInsensitive (CI)
+import Blaze.ByteString.Builder (toByteString)
+import Data.Text (Text)
+import Yesod.Message (RenderMessage (..))
+
+import Text.Blaze (toHtml, preEscapedText)
+
+-- | The type-safe URLs associated with a site argument.
+type family Route a
+
+class YesodSubRoute s y where
+ fromSubRoute :: s -> y -> Route s -> Route y
+
+data HandlerData sub master = HandlerData
+ { handlerRequest :: Request
+ , handlerSub :: sub
+ , handlerMaster :: master
+ , handlerRoute :: Maybe (Route sub)
+ , handlerRender :: Route master -> [(Text, Text)] -> Text
+ , handlerToMaster :: Route sub -> Route master
+ }
+
+handlerSubData :: (Route sub -> Route master)
+ -> (master -> sub)
+ -> Route sub
+ -> HandlerData oldSub master
+ -> HandlerData sub master
+handlerSubData tm ts = handlerSubDataMaybe tm ts . Just
+
+handlerSubDataMaybe :: (Route sub -> Route master)
+ -> (master -> sub)
+ -> Maybe (Route sub)
+ -> HandlerData oldSub master
+ -> HandlerData sub master
+handlerSubDataMaybe tm ts route hd = hd
+ { handlerSub = ts $ handlerMaster hd
+ , handlerToMaster = tm
+ , handlerRoute = route
+ }
+
+-- | Used internally for promoting subsite handler functions to master site
+-- handler functions. Should not be needed by users.
+toMasterHandler :: (Route sub -> Route master)
+ -> (master -> sub)
+ -> Route sub
+ -> GGHandler sub master mo a
+ -> GGHandler sub' master mo a
+toMasterHandler tm ts route (GHandler h) =
+ GHandler $ withReaderT (handlerSubData tm ts route) h
+
+toMasterHandlerDyn :: Monad mo
+ => (Route sub -> Route master)
+ -> GGHandler sub' master mo sub
+ -> Route sub
+ -> GGHandler sub master mo a
+ -> GGHandler sub' master mo a
+toMasterHandlerDyn tm getSub route (GHandler h) = do
+ sub <- getSub
+ GHandler $ withReaderT (handlerSubData tm (const sub) route) h
+
+class SubsiteGetter g m s | g -> s where
+ runSubsiteGetter :: g -> m s
+
+instance (master ~ master'
+ ) => SubsiteGetter (master -> sub) (GHandler anySub master') sub where
+ runSubsiteGetter getter = getter <$> getYesod
+
+instance (anySub ~ anySub'
+ ,master ~ master'
+ ) => SubsiteGetter (GHandler anySub master sub) (GHandler anySub' master') sub where
+ runSubsiteGetter = id
+
+toMasterHandlerMaybe :: (Route sub -> Route master)
+ -> (master -> sub)
+ -> Maybe (Route sub)
+ -> GGHandler sub master mo a
+ -> GGHandler sub' master mo a
+toMasterHandlerMaybe tm ts route (GHandler h) =
+ GHandler $ withReaderT (handlerSubDataMaybe tm ts route) h
+
+-- | A generic handler monad, which can have a different subsite and master
+-- site. This monad is a combination of 'ReaderT' for basic arguments, a
+-- 'WriterT' for headers and session, and an 'MEitherT' monad for handling
+-- special responses. It is declared as a newtype to make compiler errors more
+-- readable.
+newtype GGHandler sub master m a =
+ GHandler
+ { unGHandler :: GHInner sub master m a
+ }
+ deriving (Functor, Applicative, Monad, MonadIO, MonadControlIO, MonadPlus)
+
+instance MonadTrans (GGHandler s m) where
+ lift = GHandler . lift . lift . lift . lift
+
+type GHandler sub master = GGHandler sub master (Iteratee ByteString IO)
+
+data GHState = GHState
+ { ghsSession :: SessionMap
+ , ghsRBC :: Maybe RequestBodyContents
+ , ghsIdent :: Int
+ }
+
+type GHInner s m monad = -- FIXME collapse the stack
+ ReaderT (HandlerData s m) (
+ ErrorT HandlerContents (
+ WriterT (Endo [Header]) (
+ StateT GHState (
+ monad
+ ))))
+
+type SessionMap = Map.Map Text Text
+
+-- | An extension of the basic WAI 'W.Application' datatype to provide extra
+-- features needed by Yesod. Users should never need to use this directly, as
+-- the 'GHandler' monad and template haskell code should hide it away.
+newtype YesodApp = YesodApp
+ { unYesodApp
+ :: (ErrorResponse -> YesodApp)
+ -> Request
+ -> [ContentType]
+ -> SessionMap
+ -> Iteratee ByteString IO YesodAppResult
+ }
+
+data YesodAppResult
+ = YARWai W.Response
+ | YARPlain H.Status [Header] ContentType Content SessionMap
+
+data HandlerContents =
+ HCContent H.Status ChooseRep
+ | HCError ErrorResponse
+ | HCSendFile ContentType FilePath (Maybe W.FilePart) -- FIXME replace FilePath with opaque type from system-filepath?
+ | HCRedirect RedirectType Text
+ | HCCreated Text
+ | HCWai W.Response
+
+instance Error HandlerContents where
+ strMsg = HCError . InternalError . T.pack
+
+getRequest :: Monad mo => GGHandler s m mo Request
+getRequest = handlerRequest `liftM` GHandler ask
+
+instance Monad monad => Failure ErrorResponse (GGHandler sub master monad) where
+ failure = GHandler . lift . throwError . HCError
+
+runRequestBody :: GHandler s m RequestBodyContents
+runRequestBody = do
+ x <- GHandler $ lift $ lift $ lift get
+ case ghsRBC x of
+ Just rbc -> return rbc
+ Nothing -> do
+ rr <- waiRequest
+ rbc <- lift $ rbHelper rr
+ GHandler $ lift $ lift $ lift $ put x { ghsRBC = Just rbc }
+ return rbc
+
+rbHelper :: W.Request -> Iteratee ByteString IO RequestBodyContents
+rbHelper req =
+ (map fix1 *** map fix2) <$> iter
+ where
+ iter = NWP.parseRequestBody NWP.lbsSink req
+ fix1 = go *** go
+ fix2 (x, NWP.FileInfo a b c) =
+ (go x, FileInfo (go a) (go b) c)
+ go = decodeUtf8With lenientDecode
+
+-- | Get the sub application argument.
+getYesodSub :: Monad m => GGHandler sub master m sub
+getYesodSub = handlerSub `liftM` GHandler ask
+
+-- | Get the master site appliation argument.
+getYesod :: Monad m => GGHandler sub master m master
+getYesod = handlerMaster `liftM` GHandler ask
+
+-- | Get the URL rendering function.
+getUrlRender :: Monad m => GGHandler sub master m (Route master -> Text)
+getUrlRender = do
+ x <- handlerRender `liftM` GHandler ask
+ return $ flip x []
+
+-- | The URL rendering function with query-string parameters.
+getUrlRenderParams
+ :: Monad m
+ => GGHandler sub master m (Route master -> [(Text, Text)] -> Text)
+getUrlRenderParams = handlerRender `liftM` GHandler ask
+
+-- | Get the route requested by the user. If this is a 404 response- where the
+-- user requested an invalid route- this function will return 'Nothing'.
+getCurrentRoute :: Monad m => GGHandler sub master m (Maybe (Route sub))
+getCurrentRoute = handlerRoute `liftM` GHandler ask
+
+-- | Get the function to promote a route for a subsite to a route for the
+-- master site.
+getRouteToMaster :: Monad m => GGHandler sub master m (Route sub -> Route master)
+getRouteToMaster = handlerToMaster `liftM` GHandler ask
+
+-- | Function used internally by Yesod in the process of converting a
+-- 'GHandler' into an 'W.Application'. Should not be needed by users.
+runHandler :: HasReps c
+ => GHandler sub master c
+ -> (Route master -> [(Text, Text)] -> Text)
+ -> Maybe (Route sub)
+ -> (Route sub -> Route master)
+ -> master
+ -> sub
+ -> YesodApp
+runHandler handler mrender sroute tomr ma sa =
+ YesodApp $ \eh rr cts initSession -> do
+ let toErrorHandler e =
+ case fromException e of
+ Just x -> x
+ Nothing -> InternalError $ T.pack $ show e
+ let hd = HandlerData
+ { handlerRequest = rr
+ , handlerSub = sa
+ , handlerMaster = ma
+ , handlerRoute = sroute
+ , handlerRender = mrender
+ , handlerToMaster = tomr
+ }
+ let initSession' = GHState initSession Nothing 1
+ ((contents', headers), finalSession) <- catchIter (
+ fmap (second ghsSession)
+ $ flip runStateT initSession'
+ $ runWriterT
+ $ runErrorT
+ $ flip runReaderT hd
+ $ unGHandler handler
+ ) (\e -> return ((Left $ HCError $ toErrorHandler e, mempty), initSession))
+ let contents = either id (HCContent H.status200 . chooseRep) contents'
+ let handleError e = do
+ yar <- unYesodApp (eh e) safeEh rr cts finalSession
+ case yar of
+ YARPlain _ hs ct c sess ->
+ let hs' = appEndo headers hs
+ in return $ YARPlain (getStatus e) hs' ct c sess
+ YARWai _ -> return yar
+ let sendFile' ct fp p =
+ return $ YARPlain H.status200 (appEndo headers []) ct (ContentFile fp p) finalSession
+ case contents of
+ HCContent status a -> do
+ (ct, c) <- liftIO $ a cts
+ return $ YARPlain status (appEndo headers []) ct c finalSession
+ HCError e -> handleError e
+ HCRedirect rt loc -> do
+ let hs = Header "Location" (encodeUtf8 loc) : appEndo headers []
+ return $ YARPlain
+ (getRedirectStatus rt) hs typePlain emptyContent
+ finalSession
+ HCSendFile ct fp p -> catchIter
+ (sendFile' ct fp p)
+ (handleError . toErrorHandler)
+ HCCreated loc -> do
+ let hs = Header "Location" (encodeUtf8 loc) : appEndo headers []
+ return $ YARPlain
+ H.status201
+ hs
+ typePlain
+ emptyContent
+ finalSession
+ HCWai r -> return $ YARWai r
+
+catchIter :: Exception e
+ => Iteratee ByteString IO a
+ -> (e -> Iteratee ByteString IO a)
+ -> Iteratee ByteString IO a
+catchIter (Iteratee mstep) f = Iteratee $ mstep `E.catch` (runIteratee . f)
+
+safeEh :: ErrorResponse -> YesodApp
+safeEh er = YesodApp $ \_ _ _ session -> do
+ liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
+ return $ YARPlain
+ H.status500
+ []
+ typePlain
+ (toContent ("Internal Server Error" :: S.ByteString))
+ session
+
+-- | Redirect to the given route.
+redirect :: Monad mo => RedirectType -> Route master -> GGHandler sub master mo a
+redirect rt url = redirectParams rt url []
+
+-- | Redirects to the given route with the associated query-string parameters.
+redirectParams :: Monad mo
+ => RedirectType -> Route master -> [(Text, Text)]
+ -> GGHandler sub master mo a
+redirectParams rt url params = do
+ r <- getUrlRenderParams
+ redirectString rt $ r url params
+
+-- | Redirect to the given URL.
+redirectString, redirectText :: Monad mo => RedirectType -> Text -> GGHandler sub master mo a
+redirectText rt = GHandler . lift . throwError . HCRedirect rt
+redirectString = redirectText
+{-# DEPRECATED redirectString "Use redirectText instead" #-}
+
+ultDestKey :: Text
+ultDestKey = "_ULT"
+
+-- | Sets the ultimate destination variable to the given route.
+--
+-- An ultimate destination is stored in the user session and can be loaded
+-- later by 'redirectUltDest'.
+setUltDest :: Monad mo => Route master -> GGHandler sub master mo ()
+setUltDest dest = do
+ render <- getUrlRender
+ setUltDestString $ render dest
+
+-- | Same as 'setUltDest', but use the given string.
+setUltDestText :: Monad mo => Text -> GGHandler sub master mo ()
+setUltDestText = setSession ultDestKey
+
+setUltDestString :: Monad mo => Text -> GGHandler sub master mo ()
+setUltDestString = setSession ultDestKey
+{-# DEPRECATED setUltDestString "Use setUltDestText instead" #-}
+
+-- | Same as 'setUltDest', but uses the current page.
+--
+-- If this is a 404 handler, there is no current page, and then this call does
+-- nothing.
+setUltDest' :: Monad mo => GGHandler sub master mo ()
+setUltDest' = do
+ route <- getCurrentRoute
+ case route of
+ Nothing -> return ()
+ Just r -> do
+ tm <- getRouteToMaster
+ gets' <- reqGetParams `liftM` handlerRequest `liftM` GHandler ask
+ render <- getUrlRenderParams
+ setUltDestString $ render (tm r) gets'
+
+-- | Sets the ultimate destination to the referer request header, if present.
+--
+-- This function will not overwrite an existing ultdest.
+setUltDestReferer :: Monad mo => GGHandler sub master mo ()
+setUltDestReferer = do
+ mdest <- lookupSession ultDestKey
+ maybe
+ (waiRequest >>= maybe (return ()) setUltDestBS . lookup "referer" . W.requestHeaders)
+ (const $ return ())
+ mdest
+ where
+ setUltDestBS = setUltDestText . T.pack . S8.unpack
+
+-- | Redirect to the ultimate destination in the user's session. Clear the
+-- value from the session.
+--
+-- The ultimate destination is set with 'setUltDest'.
+redirectUltDest :: Monad mo
+ => RedirectType
+ -> Route master -- ^ default destination if nothing in session
+ -> GGHandler sub master mo a
+redirectUltDest rt def = do
+ mdest <- lookupSession ultDestKey
+ deleteSession ultDestKey
+ maybe (redirect rt def) (redirectText rt) mdest
+
+-- | Remove a previously set ultimate destination. See 'setUltDest'.
+clearUltDest :: Monad mo => GGHandler sub master mo ()
+clearUltDest = deleteSession ultDestKey
+
+msgKey :: Text
+msgKey = "_MSG"
+
+-- | Sets a message in the user's session.
+--
+-- See 'getMessage'.
+setMessage :: Monad mo => Html -> GGHandler sub master mo ()
+setMessage = setSession msgKey . T.concat . TL.toChunks . Text.Blaze.Renderer.Text.renderHtml
+
+-- | Sets a message in the user's session.
+--
+-- See 'getMessage'.
+setMessageI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler sub y mo ()
+setMessageI msg = do
+ mr <- getMessageRender
+ setMessage $ toHtml $ mr msg
+
+-- | Gets the message in the user's session, if available, and then clears the
+-- variable.
+--
+-- See 'setMessage'.
+getMessage :: Monad mo => GGHandler sub master mo (Maybe Html)
+getMessage = do
+ mmsg <- liftM (fmap preEscapedText) $ lookupSession msgKey
+ deleteSession msgKey
+ return mmsg
+
+-- | Bypass remaining handler code and output the given file.
+--
+-- For some backends, this is more efficient than reading in the file to
+-- memory, since they can optimize file sending via a system call to sendfile.
+sendFile :: Monad mo => ContentType -> FilePath -> GGHandler sub master mo a
+sendFile ct fp = GHandler . lift . throwError $ HCSendFile ct fp Nothing
+
+-- | Same as 'sendFile', but only sends part of a file.
+sendFilePart :: Monad mo
+ => ContentType
+ -> FilePath
+ -> Integer -- ^ offset
+ -> Integer -- ^ count
+ -> GGHandler sub master mo a
+sendFilePart ct fp off count =
+ GHandler . lift . throwError $ HCSendFile ct fp $ Just $ W.FilePart off count
+
+-- | Bypass remaining handler code and output the given content with a 200
+-- status code.
+sendResponse :: (Monad mo, HasReps c) => c -> GGHandler sub master mo a
+sendResponse = GHandler . lift . throwError . HCContent H.status200
+ . chooseRep
+
+-- | Bypass remaining handler code and output the given content with the given
+-- status code.
+sendResponseStatus :: (Monad mo, HasReps c) => H.Status -> c -> GGHandler s m mo a
+sendResponseStatus s = GHandler . lift . throwError . HCContent s
+ . chooseRep
+
+-- | Send a 201 "Created" response with the given route as the Location
+-- response header.
+sendResponseCreated :: Monad mo => Route m -> GGHandler s m mo a
+sendResponseCreated url = do
+ r <- getUrlRender
+ GHandler $ lift $ throwError $ HCCreated $ r url
+
+-- | Send a 'W.Response'. Please note: this function is rarely
+-- necessary, and will /disregard/ any changes to response headers and session
+-- that you have already specified. This function short-circuits. It should be
+-- considered only for very specific needs. If you are not sure if you need it,
+-- you don't.
+sendWaiResponse :: Monad mo => W.Response -> GGHandler s m mo b
+sendWaiResponse = GHandler . lift . throwError . HCWai
+
+-- | Return a 404 not found page. Also denotes no handler available.
+notFound :: Failure ErrorResponse m => m a
+notFound = failure NotFound
+
+-- | Return a 405 method not supported page.
+badMethod :: Monad mo => GGHandler s m mo a
+badMethod = do
+ w <- waiRequest
+ failure $ BadMethod $ W.requestMethod w
+
+-- | Return a 403 permission denied page.
+permissionDenied :: Failure ErrorResponse m => Text -> m a
+permissionDenied = failure . PermissionDenied
+
+-- | Return a 403 permission denied page.
+permissionDeniedI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler s y mo a
+permissionDeniedI msg = do
+ mr <- getMessageRender
+ permissionDenied $ mr msg
+
+-- | Return a 400 invalid arguments page.
+invalidArgs :: Failure ErrorResponse m => [Text] -> m a
+invalidArgs = failure . InvalidArgs
+
+-- | Return a 400 invalid arguments page.
+invalidArgsI :: (RenderMessage y msg, Monad mo) => [msg] -> GGHandler s y mo a
+invalidArgsI msg = do
+ mr <- getMessageRender
+ invalidArgs $ map mr msg
+
+------- Headers
+-- | Set the cookie on the client.
+setCookie :: Monad mo
+ => Int -- ^ minutes to timeout
+ -> H.Ascii -- ^ key
+ -> H.Ascii -- ^ value
+ -> GGHandler sub master mo ()
+setCookie a b = addHeader . AddCookie a b
+
+-- | Unset the cookie on the client.
+deleteCookie :: Monad mo => H.Ascii -> GGHandler sub master mo ()
+deleteCookie = addHeader . DeleteCookie
+
+-- | Set the language in the user session. Will show up in 'languages' on the
+-- next request.
+setLanguage :: Monad mo => Text -> GGHandler sub master mo ()
+setLanguage = setSession langKey
+
+-- | Set an arbitrary response header.
+setHeader :: Monad mo
+ => CI H.Ascii -> H.Ascii -> GGHandler sub master mo ()
+setHeader a = addHeader . Header a
+
+-- | Set the Cache-Control header to indicate this response should be cached
+-- for the given number of seconds.
+cacheSeconds :: Monad mo => Int -> GGHandler s m mo ()
+cacheSeconds i = setHeader "Cache-Control" $ S8.pack $ concat
+ [ "max-age="
+ , show i
+ , ", public"
+ ]
+
+-- | Set the Expires header to some date in 2037. In other words, this content
+-- is never (realistically) expired.
+neverExpires :: Monad mo => GGHandler s m mo ()
+neverExpires = setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT"
+
+-- | Set an Expires header in the past, meaning this content should not be
+-- cached.
+alreadyExpired :: Monad mo => GGHandler s m mo ()
+alreadyExpired = setHeader "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
+
+-- | Set an Expires header to the given date.
+expiresAt :: Monad mo => UTCTime -> GGHandler s m mo ()
+expiresAt = setHeader "Expires" . encodeUtf8 . formatRFC1123
+
+-- | Set a variable in the user's session.
+--
+-- The session is handled by the clientsession package: it sets an encrypted
+-- and hashed cookie on the client. This ensures that all data is secure and
+-- not tampered with.
+setSession :: Monad mo
+ => Text -- ^ key
+ -> Text -- ^ value
+ -> GGHandler sub master mo ()
+setSession k = GHandler . lift . lift . lift . modify . modSession . Map.insert k
+
+-- | Unsets a session variable. See 'setSession'.
+deleteSession :: Monad mo => Text -> GGHandler sub master mo ()
+deleteSession = GHandler . lift . lift . lift . modify . modSession . Map.delete
+
+modSession :: (SessionMap -> SessionMap) -> GHState -> GHState
+modSession f x = x { ghsSession = f $ ghsSession x }
+
+-- | Internal use only, not to be confused with 'setHeader'.
+addHeader :: Monad mo => Header -> GGHandler sub master mo ()
+addHeader = GHandler . lift . lift . tell . Endo . (:)
+
+getStatus :: ErrorResponse -> H.Status
+getStatus NotFound = H.status404
+getStatus (InternalError _) = H.status500
+getStatus (InvalidArgs _) = H.status400
+getStatus (PermissionDenied _) = H.status403
+getStatus (BadMethod _) = H.status405
+
+getRedirectStatus :: RedirectType -> H.Status
+getRedirectStatus RedirectPermanent = H.status301
+getRedirectStatus RedirectTemporary = H.status302
+getRedirectStatus RedirectSeeOther = H.status303
+
+-- | Different types of redirects.
+data RedirectType = RedirectPermanent
+ | RedirectTemporary
+ | RedirectSeeOther
+ deriving (Show, Eq)
+
+localNoCurrent :: Monad mo => GGHandler s m mo a -> GGHandler s m mo a
+localNoCurrent =
+ GHandler . local (\hd -> hd { handlerRoute = Nothing }) . unGHandler
+
+-- | Lookup for session data.
+lookupSession :: Monad mo => Text -> GGHandler s m mo (Maybe Text)
+lookupSession n = GHandler $ do
+ m <- liftM ghsSession $ lift $ lift $ lift get
+ return $ Map.lookup n m
+
+-- | Get all session variables.
+getSession :: Monad mo => GGHandler s m mo SessionMap
+getSession = liftM ghsSession $ GHandler $ lift $ lift $ lift get
+
+handlerToYAR :: (HasReps a, HasReps b)
+ => m -- ^ master site foundation
+ -> s -- ^ sub site foundation
+ -> (Route s -> Route m)
+ -> (Route m -> [(Text, Text)] -> Text)
+ -> (ErrorResponse -> GHandler s m a)
+ -> Request
+ -> Maybe (Route s)
+ -> SessionMap
+ -> GHandler s m b
+ -> Iteratee ByteString IO YesodAppResult
+handlerToYAR y s toMasterRoute render errorHandler rr murl sessionMap h =
+ unYesodApp ya eh' rr types sessionMap
+ where
+ ya = runHandler h render murl toMasterRoute y s
+ eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s
+ types = httpAccept $ reqWaiRequest rr
+ errorHandler' = localNoCurrent . errorHandler
+
+type HeaderRenderer = [Header]
+ -> ContentType
+ -> SessionMap
+ -> [(CI H.Ascii, H.Ascii)]
+
+yarToResponse :: HeaderRenderer -> YesodAppResult -> W.Response
+yarToResponse _ (YARWai a) = a
+yarToResponse renderHeaders (YARPlain s hs ct c sessionFinal) =
+ case c of
+ ContentBuilder b mlen ->
+ let hs' = maybe finalHeaders finalHeaders' mlen
+ in W.ResponseBuilder s hs' b
+ ContentFile fp p -> W.ResponseFile s finalHeaders fp p
+ ContentEnum e ->
+ W.ResponseEnumerator $ \iter -> run_ $ e $$ iter s finalHeaders
+ where
+ finalHeaders = renderHeaders hs ct sessionFinal
+ finalHeaders' len = ("Content-Length", S8.pack $ show len)
+ : finalHeaders
+ {-
+ getExpires m = fromIntegral (m * 60) `addUTCTime` now
+ sessionVal =
+ case key' of
+ Nothing -> B.empty
+ Just key'' -> encodeSession key'' exp' host
+ $ Map.toList
+ $ Map.insert nonceKey (reqNonce rr) sessionFinal
+ hs' =
+ case key' of
+ Nothing -> hs
+ Just _ -> AddCookie
+ (clientSessionDuration y)
+ sessionName
+ (bsToChars sessionVal)
+ : hs
+ hs'' = map (headerToPair getExpires) hs'
+ hs''' = ("Content-Type", charsToBs ct) : hs''
+ -}
+
+httpAccept :: W.Request -> [ContentType]
+httpAccept = parseHttpAccept
+ . fromMaybe mempty
+ . lookup "Accept"
+ . W.requestHeaders
+
+-- | Convert Header to a key/value pair.
+headerToPair :: S.ByteString -- ^ cookie path
+ -> (Int -> UTCTime) -- ^ minutes -> expiration time
+ -> Header
+ -> (CI H.Ascii, H.Ascii)
+headerToPair cp getExpires (AddCookie minutes key value) =
+ ("Set-Cookie", toByteString $ renderSetCookie $ SetCookie
+ { setCookieName = key
+ , setCookieValue = value
+ , setCookiePath = Just cp
+ , setCookieExpires =
+ if minutes == 0
+ then Nothing
+ else Just $ getExpires minutes
+ , setCookieDomain = Nothing
+ , setCookieHttpOnly = True
+ })
+headerToPair cp _ (DeleteCookie key) =
+ ( "Set-Cookie"
+ , key `mappend` "=; path=" `mappend` cp `mappend` "; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ )
+headerToPair _ _ (Header key value) = (key, value)
+
+-- | Get a unique identifier.
+newIdent :: Monad mo => GGHandler sub master mo String -- FIXME use Text
+newIdent = GHandler $ lift $ lift $ lift $ do
+ x <- get
+ let i' = ghsIdent x + 1
+ put x { ghsIdent = i' }
+ return $ 'h' : show i'
+
+liftIOHandler :: MonadIO mo
+ => GGHandler sub master IO a
+ -> GGHandler sub master mo a
+liftIOHandler m = GHandler $
+ ReaderT $ \r ->
+ ErrorT $
+ WriterT $
+ StateT $ \s ->
+ liftIO $ runGGHandler m r s
+
+runGGHandler :: GGHandler sub master m a
+ -> HandlerData sub master
+ -> GHState
+ -> m ( ( Either HandlerContents a
+ , Endo [Header]
+ )
+ , GHState
+ )
+runGGHandler m r s = runStateT
+ (runWriterT
+ (runErrorT
+ (runReaderT
+ (unGHandler m) r))) s
+
+instance MonadTransControl (GGHandler s m) where
+ liftControl f =
+ GHandler $
+ liftControl $ \runRdr ->
+ liftControl $ \runErr ->
+ liftControl $ \runWrt ->
+ liftControl $ \runSt ->
+ f ( liftM ( GHandler
+ . join . lift
+ . join . lift
+ . join . lift
+ )
+ . runSt . runWrt . runErr . runRdr
+ . unGHandler
+ )
+
+-- | Redirect to a POST resource.
+--
+-- This is not technically a redirect; instead, it returns an HTML page with a
+-- POST form, and some Javascript to automatically submit the form. This can be
+-- useful when you need to post a plain link somewhere that needs to cause
+-- changes on the server.
+redirectToPost :: Monad mo => Route master -> GGHandler sub master mo a
+redirectToPost dest = hamletToRepHtml
+#if GHC7
+ [hamlet|
+#else
+ [$hamlet|
+#endif
+\
+
+
+
+ Redirecting...
+
+