260 lines
8.5 KiB
Haskell
260 lines
8.5 KiB
Haskell
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE TypeSynonymInstances #-}
|
|
{-# LANGUAGE DeriveDataTypeable #-}
|
|
{-# LANGUAGE CPP #-}
|
|
---------------------------------------------------------
|
|
--
|
|
-- Module : Yesod.Request
|
|
-- Copyright : Michael Snoyman
|
|
-- License : BSD3
|
|
--
|
|
-- Maintainer : Michael Snoyman <michael@snoyman.com>
|
|
-- Stability : Stable
|
|
-- Portability : portable
|
|
--
|
|
-- Code for extracting parameters from requests.
|
|
--
|
|
---------------------------------------------------------
|
|
module Yesod.Request
|
|
(
|
|
-- * RawRequest
|
|
RawRequest (..)
|
|
-- * Parameter type class
|
|
-- * MonadRequestReader type class and helpers
|
|
, RequestReader (..)
|
|
, getParam
|
|
, postParam
|
|
, anyParam
|
|
, cookieParam
|
|
, identifier
|
|
, displayName
|
|
, acceptedLanguages
|
|
, requestPath
|
|
, parseEnv
|
|
, runRequest
|
|
-- * Building actual request
|
|
, Request (..)
|
|
, Hack.RequestMethod (..)
|
|
-- * Parameter restrictions
|
|
-- FIXME , notBlank
|
|
#if TEST
|
|
, testSuite
|
|
#endif
|
|
) where
|
|
|
|
import qualified Hack
|
|
import Data.Function.Predicate (equals)
|
|
import Yesod.Constants
|
|
import Yesod.Utils (tryLookup, parseHttpAccept)
|
|
import Yesod.Definitions
|
|
import Yesod.Parameter
|
|
import Control.Applicative (Applicative (..))
|
|
import Web.Encodings
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import Data.Convertible.Text
|
|
import Hack.Middleware.CleanPath (splitPath)
|
|
import Control.Arrow ((***))
|
|
import Control.Exception (Exception, SomeException (..))
|
|
import Data.Typeable (Typeable)
|
|
import Data.Attempt
|
|
|
|
#if TEST
|
|
import Test.Framework (testGroup, Test)
|
|
import Test.Framework.Providers.HUnit
|
|
import Test.HUnit hiding (Test)
|
|
#endif
|
|
|
|
newtype Request v = Request { unRequest :: RawRequest
|
|
-> Either ParamException v }
|
|
instance Functor Request where
|
|
fmap f (Request r) = Request $ fmap f . r
|
|
instance Applicative Request where
|
|
pure = Request . const . Right
|
|
(Request f) <*> (Request r) = Request helper where
|
|
helper rr = helper2 (f rr) (r rr)
|
|
helper2 (Left e1) (Left e2) = Left $ e1 ++ e2
|
|
helper2 (Left e) _ = Left e
|
|
helper2 _ (Left e) = Left e
|
|
helper2 (Right f') (Right r') = Right $ f' r'
|
|
|
|
class RequestReader m where
|
|
getRawRequest :: m RawRequest
|
|
invalidParams :: ParamException -> m a
|
|
instance RequestReader Request where
|
|
getRawRequest = Request $ Right
|
|
invalidParams = Request . const . Left
|
|
|
|
runRequest :: (Monad m, RequestReader m) => Request a -> m a
|
|
runRequest (Request f) = do
|
|
rr <- getRawRequest
|
|
either invalidParams return $ f rr
|
|
{- FIXME
|
|
-- | Attempt to parse a list of param values using 'readParams'.
|
|
-- If that fails, return an error message and an undefined value. This way,
|
|
-- we can process all of the parameters and get all of the error messages.
|
|
-- Be careful not to use the value inside until you can be certain the
|
|
-- reading succeeded.
|
|
tryReadParams:: (Parameter a, MonadRequestReader m)
|
|
=> ParamType
|
|
-> ParamName
|
|
-> [RawParam]
|
|
-> m a
|
|
tryReadParams ptype name params =
|
|
case readParams params of
|
|
Failure s -> invalidParam ptype name s
|
|
Success x -> return x
|
|
-}
|
|
|
|
-- | Helper function for generating 'RequestParser's from various
|
|
-- 'ParamValue' lists.
|
|
genParam :: Parameter a
|
|
=> (RawRequest -> ParamName -> [ParamValue])
|
|
-> ParamType
|
|
-> ParamName
|
|
-> Request a
|
|
genParam f ptype name = Request helper where
|
|
helper req = attempt failureH Right $ readParams pvs where
|
|
pvs = f req name
|
|
failureH e = Left [((ptype, name, pvs), SomeException e)]
|
|
|
|
-- | Parse a value passed as a GET parameter.
|
|
getParam :: (Parameter a) => ParamName -> Request a
|
|
getParam = genParam getParams GetParam
|
|
|
|
-- | Parse a value passed as a POST parameter.
|
|
postParam :: (Parameter a) => ParamName -> Request a
|
|
postParam = genParam postParams PostParam
|
|
|
|
-- | Parse a value passed as a GET, POST or URL parameter.
|
|
anyParam :: (Parameter a) => ParamName -> Request a
|
|
anyParam = genParam anyParams PostParam -- FIXME
|
|
|
|
-- | Parse a value passed as a raw cookie.
|
|
cookieParam :: (Parameter a) => ParamName -> Request a
|
|
cookieParam = genParam cookies CookieParam
|
|
|
|
-- | Extract the cookie which specifies the identifier for a logged in
|
|
-- user, if available.
|
|
identifier :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
|
|
identifier = do
|
|
env <- parseEnv
|
|
case lookup authCookieName $ Hack.hackHeaders env of
|
|
Nothing -> return Nothing
|
|
Just x -> return (Just x)
|
|
|
|
displayName :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
|
|
displayName = do
|
|
env <- parseEnv
|
|
case lookup authDisplayName $ Hack.hackHeaders env of
|
|
Nothing -> return Nothing
|
|
Just x -> return (Just x)
|
|
|
|
-- | Get the raw 'Hack.Env' value.
|
|
parseEnv :: (Functor m, RequestReader m) => m Hack.Env
|
|
parseEnv = rawEnv `fmap` getRawRequest
|
|
|
|
-- | Determine the ordered list of language preferences.
|
|
--
|
|
-- FIXME: Future versions should account for some cookie.
|
|
acceptedLanguages :: (Functor m, Monad m, RequestReader m) => m [String]
|
|
acceptedLanguages = do
|
|
env <- parseEnv
|
|
let rawLang = tryLookup "" "Accept-Language" $ Hack.http env
|
|
return $! parseHttpAccept rawLang
|
|
|
|
-- | Determinge the path requested by the user (ie, the path info).
|
|
requestPath :: (Functor m, Monad m, RequestReader m) => m String
|
|
requestPath = do
|
|
env <- parseEnv
|
|
let q = case Hack.queryString env of
|
|
"" -> ""
|
|
q'@('?':_) -> q'
|
|
q' -> q'
|
|
return $! dropSlash (Hack.pathInfo env) ++ q
|
|
where
|
|
dropSlash ('/':x) = x
|
|
dropSlash x = x
|
|
|
|
-- | The raw information passed through Hack, cleaned up a bit.
|
|
data RawRequest = RawRequest
|
|
{ rawPathInfo :: PathInfo
|
|
, rawGetParams :: [(ParamName, ParamValue)]
|
|
, rawPostParams :: [(ParamName, ParamValue)]
|
|
, rawCookies :: [(ParamName, ParamValue)]
|
|
, rawFiles :: [(ParamName, FileInfo String BL.ByteString)]
|
|
, rawEnv :: Hack.Env
|
|
, rawLanguages :: [Language]
|
|
}
|
|
deriving Show
|
|
|
|
-- | All GET paramater values with the given name.
|
|
getParams :: RawRequest -> ParamName -> [ParamValue]
|
|
getParams rr name = map snd
|
|
. filter (\x -> name == fst x)
|
|
. rawGetParams
|
|
$ rr
|
|
|
|
-- | All POST paramater values with the given name.
|
|
postParams :: RawRequest -> ParamName -> [ParamValue]
|
|
postParams rr name = map snd
|
|
. filter (\x -> name == fst x)
|
|
. rawPostParams
|
|
$ rr
|
|
|
|
-- | All GET and POST paramater values (see rewriting) with the given name.
|
|
anyParams :: RawRequest -> ParamName -> [ParamValue]
|
|
anyParams req name = getParams req name ++
|
|
postParams req name
|
|
|
|
-- | All cookies with the given name.
|
|
cookies :: RawRequest -> ParamName -> [ParamValue]
|
|
cookies rr name = map snd . filter (fst `equals` name) . rawCookies $ rr
|
|
|
|
{- FIXME
|
|
-- | Ensures that a String parameter is not blank.
|
|
notBlank :: MonadRequestReader m => RawParam -> m String
|
|
notBlank rp =
|
|
case paramValue rp of
|
|
"" -> invalidParam (paramType rp) (paramName rp) RequiredField
|
|
s -> return s
|
|
-}
|
|
|
|
data RequiredField = RequiredField
|
|
deriving (Show, Typeable)
|
|
instance Exception RequiredField
|
|
|
|
instance ConvertSuccess Hack.Env RawRequest where
|
|
convertSuccess env =
|
|
let (Right rawPieces) = splitPath $ Hack.pathInfo env
|
|
gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
|
|
clength = tryLookup "0" "Content-Length" $ Hack.http env
|
|
ctype = tryLookup "" "Content-Type" $ Hack.http env
|
|
convertFileInfo (FileInfo a b c) = FileInfo (cs a) (cs b) c
|
|
(posts, files) = map (convertSuccess *** convertSuccess) ***
|
|
map (convertSuccess *** convertFileInfo)
|
|
$ parsePost ctype clength
|
|
$ Hack.hackInput env
|
|
rawCookie = tryLookup "" "Cookie" $ Hack.http env
|
|
cookies' = decodeCookies rawCookie :: [(String, String)]
|
|
langs = ["en"] -- FIXME
|
|
in RawRequest rawPieces gets' posts cookies' files env langs
|
|
|
|
#if TEST
|
|
testSuite :: Test
|
|
testSuite = testGroup "Yesod.Request"
|
|
[ testCase "Request applicative instance" caseAppInst
|
|
]
|
|
|
|
caseAppInst :: Assertion
|
|
caseAppInst = do
|
|
let r5 = Request $ const $ Right 5
|
|
rAdd2 = Request $ const $ Right (+ 2)
|
|
r7 = Request $ const $ Right 7
|
|
rr = undefined
|
|
myEquals e t = (unRequest e) rr `myEquals2` (unRequest t) rr
|
|
myEquals2 x y = show x @=? show y
|
|
r5 `myEquals` pure 5
|
|
r7 `myEquals` (rAdd2 <*> r5)
|
|
#endif
|