FileUpload (#358)
This commit is contained in:
parent
21a4360f74
commit
8fac4917b5
@ -181,6 +181,8 @@ import Control.Monad.Trans.Control
|
|||||||
import Control.Monad.Trans.Resource
|
import Control.Monad.Trans.Resource
|
||||||
import Control.Monad.Base
|
import Control.Monad.Base
|
||||||
import Yesod.Routes.Class
|
import Yesod.Routes.Class
|
||||||
|
import Data.Word (Word64)
|
||||||
|
import Data.Conduit (Sink)
|
||||||
|
|
||||||
class YesodSubRoute s y where
|
class YesodSubRoute s y where
|
||||||
fromSubRoute :: s -> y -> Route s -> Route y
|
fromSubRoute :: s -> y -> Route s -> Route y
|
||||||
@ -193,6 +195,7 @@ data HandlerData sub master = HandlerData
|
|||||||
, handlerRender :: Route master -> [(Text, Text)] -> Text
|
, handlerRender :: Route master -> [(Text, Text)] -> Text
|
||||||
, handlerToMaster :: Route sub -> Route master
|
, handlerToMaster :: Route sub -> Route master
|
||||||
, handlerState :: I.IORef GHState
|
, handlerState :: I.IORef GHState
|
||||||
|
, handlerUpload :: Word64 -> FileUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerSubData :: (Route sub -> Route master)
|
handlerSubData :: (Route sub -> Route master)
|
||||||
@ -322,22 +325,35 @@ hcError = liftIO . throwIO . HCError
|
|||||||
|
|
||||||
runRequestBody :: GHandler s m RequestBodyContents
|
runRequestBody :: GHandler s m RequestBodyContents
|
||||||
runRequestBody = do
|
runRequestBody = do
|
||||||
|
hd <- ask
|
||||||
|
let getUpload = handlerUpload hd
|
||||||
|
len = reqBodySize $ handlerRequest hd
|
||||||
|
upload = getUpload len
|
||||||
x <- get
|
x <- get
|
||||||
case ghsRBC x of
|
case ghsRBC x of
|
||||||
Just rbc -> return rbc
|
Just rbc -> return rbc
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
rr <- waiRequest
|
rr <- waiRequest
|
||||||
rbc <- lift $ rbHelper rr
|
rbc <- lift $ rbHelper upload rr
|
||||||
put x { ghsRBC = Just rbc }
|
put x { ghsRBC = Just rbc }
|
||||||
return rbc
|
return rbc
|
||||||
|
|
||||||
rbHelper :: W.Request -> ResourceT IO RequestBodyContents
|
rbHelper :: FileUpload -> W.Request -> ResourceT IO RequestBodyContents
|
||||||
rbHelper req =
|
rbHelper upload =
|
||||||
(map fix1 *** map fix2) <$> (NWP.parseRequestBody NWP.lbsSink req) -- FIXME allow control over which backend to use
|
case upload of
|
||||||
|
FileUploadMemory s -> rbHelper' s mkFileInfoLBS
|
||||||
|
FileUploadDisk s -> rbHelper' s mkFileInfoFile
|
||||||
|
|
||||||
|
rbHelper' :: Sink S8.ByteString (ResourceT IO) x
|
||||||
|
-> (Text -> Text -> x -> FileInfo)
|
||||||
|
-> W.Request
|
||||||
|
-> ResourceT IO ([(Text, Text)], [(Text, FileInfo)])
|
||||||
|
rbHelper' sink mkFI req =
|
||||||
|
(map fix1 *** map fix2) <$> (NWP.parseRequestBody sink req)
|
||||||
where
|
where
|
||||||
fix1 = go *** go
|
fix1 = go *** go
|
||||||
fix2 (x, NWP.FileInfo a b c) =
|
fix2 (x, NWP.FileInfo a b c) =
|
||||||
(go x, FileInfo (go a) (go b) c)
|
(go x, mkFI (go a) (go b) c)
|
||||||
go = decodeUtf8With lenientDecode
|
go = decodeUtf8With lenientDecode
|
||||||
|
|
||||||
-- | Get the sub application argument.
|
-- | Get the sub application argument.
|
||||||
@ -378,8 +394,9 @@ runHandler :: HasReps c
|
|||||||
-> (Route sub -> Route master)
|
-> (Route sub -> Route master)
|
||||||
-> master
|
-> master
|
||||||
-> sub
|
-> sub
|
||||||
|
-> (Word64 -> FileUpload)
|
||||||
-> YesodApp
|
-> YesodApp
|
||||||
runHandler handler mrender sroute tomr master sub =
|
runHandler handler mrender sroute tomr master sub upload =
|
||||||
YesodApp $ \eh rr cts initSession -> do
|
YesodApp $ \eh rr cts initSession -> do
|
||||||
let toErrorHandler e =
|
let toErrorHandler e =
|
||||||
case fromException e of
|
case fromException e of
|
||||||
@ -400,6 +417,7 @@ runHandler handler mrender sroute tomr master sub =
|
|||||||
, handlerRender = mrender
|
, handlerRender = mrender
|
||||||
, handlerToMaster = tomr
|
, handlerToMaster = tomr
|
||||||
, handlerState = istate
|
, handlerState = istate
|
||||||
|
, handlerUpload = upload
|
||||||
}
|
}
|
||||||
contents' <- catch (fmap Right $ unGHandler handler hd)
|
contents' <- catch (fmap Right $ unGHandler handler hd)
|
||||||
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
|
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
|
||||||
@ -772,6 +790,7 @@ getSession = liftM ghsSession get
|
|||||||
handlerToYAR :: (HasReps a, HasReps b)
|
handlerToYAR :: (HasReps a, HasReps b)
|
||||||
=> master -- ^ master site foundation
|
=> master -- ^ master site foundation
|
||||||
-> sub -- ^ sub site foundation
|
-> sub -- ^ sub site foundation
|
||||||
|
-> (Word64 -> FileUpload)
|
||||||
-> (Route sub -> Route master)
|
-> (Route sub -> Route master)
|
||||||
-> (Route master -> [(Text, Text)] -> Text) -- route renderer
|
-> (Route master -> [(Text, Text)] -> Text) -- route renderer
|
||||||
-> (ErrorResponse -> GHandler sub master a)
|
-> (ErrorResponse -> GHandler sub master a)
|
||||||
@ -780,11 +799,11 @@ handlerToYAR :: (HasReps a, HasReps b)
|
|||||||
-> SessionMap
|
-> SessionMap
|
||||||
-> GHandler sub master b
|
-> GHandler sub master b
|
||||||
-> ResourceT IO YesodAppResult
|
-> ResourceT IO YesodAppResult
|
||||||
handlerToYAR y s toMasterRoute render errorHandler rr murl sessionMap h =
|
handlerToYAR y s upload toMasterRoute render errorHandler rr murl sessionMap h =
|
||||||
unYesodApp ya eh' rr types sessionMap
|
unYesodApp ya eh' rr types sessionMap
|
||||||
where
|
where
|
||||||
ya = runHandler h render murl toMasterRoute y s
|
ya = runHandler h render murl toMasterRoute y s upload
|
||||||
eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s
|
eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s upload
|
||||||
types = httpAccept $ reqWaiRequest rr
|
types = httpAccept $ reqWaiRequest rr
|
||||||
errorHandler' = localNoCurrent . errorHandler
|
errorHandler' = localNoCurrent . errorHandler
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ module Yesod.Internal.Core
|
|||||||
, yesodRender
|
, yesodRender
|
||||||
, resolveApproot
|
, resolveApproot
|
||||||
, Approot (..)
|
, Approot (..)
|
||||||
|
, FileUpload (..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Yesod.Content
|
import Yesod.Content
|
||||||
@ -90,6 +91,7 @@ import Data.Aeson (Value (Array, String))
|
|||||||
import Data.Aeson.Encode (encode)
|
import Data.Aeson.Encode (encode)
|
||||||
import qualified Data.Vector as Vector
|
import qualified Data.Vector as Vector
|
||||||
import Network.Wai.Middleware.Gzip (GzipSettings, def)
|
import Network.Wai.Middleware.Gzip (GzipSettings, def)
|
||||||
|
import Network.Wai.Parse (tempFileSink, lbsSink)
|
||||||
import qualified Paths_yesod_core
|
import qualified Paths_yesod_core
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
|
||||||
@ -325,6 +327,16 @@ $doctype 5
|
|||||||
key <- CS.getKey CS.defaultKeyFile
|
key <- CS.getKey CS.defaultKeyFile
|
||||||
return $ Just $ clientSessionBackend key 120
|
return $ Just $ clientSessionBackend key 120
|
||||||
|
|
||||||
|
-- | How to store uploaded files.
|
||||||
|
--
|
||||||
|
-- Default: Whe nthe request body is greater than 50kb, store in a temp
|
||||||
|
-- file. Otherwise, store in memory.
|
||||||
|
fileUpload :: a
|
||||||
|
-> Word64 -- ^ request body size
|
||||||
|
-> FileUpload
|
||||||
|
fileUpload _ size
|
||||||
|
| size > 50000 = FileUploadDisk tempFileSink
|
||||||
|
| otherwise = FileUploadMemory lbsSink
|
||||||
|
|
||||||
messageLoggerHandler :: Yesod m
|
messageLoggerHandler :: Yesod m
|
||||||
=> Loc -> LogLevel -> Text -> GHandler s m ()
|
=> Loc -> LogLevel -> Text -> GHandler s m ()
|
||||||
@ -376,24 +388,18 @@ defaultYesodRunner :: Yesod master
|
|||||||
-> (Route sub -> Route master)
|
-> (Route sub -> Route master)
|
||||||
-> Maybe (SessionBackend master)
|
-> Maybe (SessionBackend master)
|
||||||
-> W.Application
|
-> W.Application
|
||||||
defaultYesodRunner _ master _ murl toMaster _ req
|
defaultYesodRunner handler master sub murl toMasterRoute msb req
|
||||||
| maximumContentLength master (fmap toMaster murl) < len =
|
| maximumContentLength master (fmap toMasterRoute murl) < len =
|
||||||
return $ W.responseLBS
|
return $ W.responseLBS
|
||||||
(H.Status 413 "Too Large")
|
(H.Status 413 "Too Large")
|
||||||
[("Content-Type", "text/plain")]
|
[("Content-Type", "text/plain")]
|
||||||
"Request body too large to be processed."
|
"Request body too large to be processed."
|
||||||
where
|
| otherwise = do
|
||||||
len = fromMaybe 0 $ lookup "content-length" (W.requestHeaders req) >>= readMay
|
|
||||||
readMay s =
|
|
||||||
case reads $ S8.unpack s of
|
|
||||||
[] -> Nothing
|
|
||||||
(x, _):_ -> Just x
|
|
||||||
defaultYesodRunner handler master sub murl toMasterRoute msb req = do
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let dontSaveSession _ _ = return []
|
let dontSaveSession _ _ = return []
|
||||||
(session, saveSession) <- liftIO $
|
(session, saveSession) <- liftIO $
|
||||||
maybe (return ([], dontSaveSession)) (\sb -> sbLoadSession sb master req now) msb
|
maybe (return ([], dontSaveSession)) (\sb -> sbLoadSession sb master req now) msb
|
||||||
rr <- liftIO $ parseWaiRequest req session (isJust msb)
|
rr <- liftIO $ parseWaiRequest req session (isJust msb) len
|
||||||
let h = {-# SCC "h" #-} do
|
let h = {-# SCC "h" #-} do
|
||||||
case murl of
|
case murl of
|
||||||
Nothing -> handler
|
Nothing -> handler
|
||||||
@ -413,7 +419,7 @@ defaultYesodRunner handler master sub murl toMasterRoute msb req = do
|
|||||||
handler
|
handler
|
||||||
let sessionMap = Map.fromList . filter ((/=) tokenKey . fst) $ session
|
let sessionMap = Map.fromList . filter ((/=) tokenKey . fst) $ session
|
||||||
let ra = resolveApproot master req
|
let ra = resolveApproot master req
|
||||||
yar <- handlerToYAR master sub toMasterRoute
|
yar <- handlerToYAR master sub (fileUpload master) toMasterRoute
|
||||||
(yesodRender master ra) errorHandler rr murl sessionMap h
|
(yesodRender master ra) errorHandler rr murl sessionMap h
|
||||||
extraHeaders <- case yar of
|
extraHeaders <- case yar of
|
||||||
(YARPlain _ _ ct _ newSess) -> do
|
(YARPlain _ _ ct _ newSess) -> do
|
||||||
@ -425,6 +431,12 @@ defaultYesodRunner handler master sub murl toMasterRoute msb req = do
|
|||||||
return $ ("Content-Type", ct) : map headerToPair sessionHeaders
|
return $ ("Content-Type", ct) : map headerToPair sessionHeaders
|
||||||
_ -> return []
|
_ -> return []
|
||||||
return $ yarToResponse yar extraHeaders
|
return $ yarToResponse yar extraHeaders
|
||||||
|
where
|
||||||
|
len = fromMaybe 0 $ lookup "content-length" (W.requestHeaders req) >>= readMay
|
||||||
|
readMay s =
|
||||||
|
case reads $ S8.unpack s of
|
||||||
|
[] -> Nothing
|
||||||
|
(x, _):_ -> Just x
|
||||||
|
|
||||||
data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text
|
data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text
|
||||||
deriving (Eq, Show, Read)
|
deriving (Eq, Show, Read)
|
||||||
|
|||||||
@ -4,7 +4,14 @@ module Yesod.Internal.Request
|
|||||||
( parseWaiRequest
|
( parseWaiRequest
|
||||||
, Request (..)
|
, Request (..)
|
||||||
, RequestBodyContents
|
, RequestBodyContents
|
||||||
, FileInfo (..)
|
, FileInfo
|
||||||
|
, fileName
|
||||||
|
, fileContentType
|
||||||
|
, fileSource
|
||||||
|
, fileMove
|
||||||
|
, mkFileInfoLBS
|
||||||
|
, mkFileInfoFile
|
||||||
|
, FileUpload (..)
|
||||||
-- The below are exported for testing.
|
-- The below are exported for testing.
|
||||||
, randomString
|
, randomString
|
||||||
, parseWaiRequest'
|
, parseWaiRequest'
|
||||||
@ -28,6 +35,10 @@ import qualified Data.Set as Set
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeUtf8With)
|
import Data.Text.Encoding (decodeUtf8With)
|
||||||
import Data.Text.Encoding.Error (lenientDecode)
|
import Data.Text.Encoding.Error (lenientDecode)
|
||||||
|
import Data.Conduit
|
||||||
|
import Data.Conduit.List (sourceList)
|
||||||
|
import Data.Conduit.Binary (sourceFile, sinkFile)
|
||||||
|
import Data.Word (Word64)
|
||||||
|
|
||||||
-- | The parsed request information.
|
-- | The parsed request information.
|
||||||
data Request = Request
|
data Request = Request
|
||||||
@ -38,23 +49,27 @@ data Request = Request
|
|||||||
, reqLangs :: [Text]
|
, reqLangs :: [Text]
|
||||||
-- | A random, session-specific token used to prevent CSRF attacks.
|
-- | A random, session-specific token used to prevent CSRF attacks.
|
||||||
, reqToken :: Maybe Text
|
, reqToken :: Maybe Text
|
||||||
|
-- | Size of the request body.
|
||||||
|
, reqBodySize :: Word64
|
||||||
}
|
}
|
||||||
|
|
||||||
parseWaiRequest :: W.Request
|
parseWaiRequest :: W.Request
|
||||||
-> [(Text, ByteString)] -- ^ session
|
-> [(Text, ByteString)] -- ^ session
|
||||||
-> Bool
|
-> Bool
|
||||||
|
-> Word64
|
||||||
-> IO Request
|
-> IO Request
|
||||||
parseWaiRequest env session' useToken =
|
parseWaiRequest env session' useToken bodySize =
|
||||||
parseWaiRequest' env session' useToken <$> newStdGen
|
parseWaiRequest' env session' useToken bodySize <$> newStdGen
|
||||||
|
|
||||||
parseWaiRequest' :: RandomGen g
|
parseWaiRequest' :: RandomGen g
|
||||||
=> W.Request
|
=> W.Request
|
||||||
-> [(Text, ByteString)] -- ^ session
|
-> [(Text, ByteString)] -- ^ session
|
||||||
-> Bool
|
-> Bool
|
||||||
|
-> Word64
|
||||||
-> g
|
-> g
|
||||||
-> Request
|
-> Request
|
||||||
parseWaiRequest' env session' useToken gen =
|
parseWaiRequest' env session' useToken bodySize gen =
|
||||||
Request gets'' cookies' env langs'' token
|
Request gets'' cookies' env langs'' token bodySize
|
||||||
where
|
where
|
||||||
gets' = queryToQueryText $ W.queryString env
|
gets' = queryToQueryText $ W.queryString env
|
||||||
gets'' = map (second $ fromMaybe "") gets'
|
gets'' = map (second $ fromMaybe "") gets'
|
||||||
@ -116,6 +131,15 @@ type RequestBodyContents =
|
|||||||
data FileInfo = FileInfo
|
data FileInfo = FileInfo
|
||||||
{ fileName :: Text
|
{ fileName :: Text
|
||||||
, fileContentType :: Text
|
, fileContentType :: Text
|
||||||
, fileContent :: L.ByteString
|
, fileSource :: Source (ResourceT IO) ByteString
|
||||||
|
, fileMove :: FilePath -> IO ()
|
||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
|
||||||
|
mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo
|
||||||
|
mkFileInfoLBS name ct lbs = FileInfo name ct (sourceList $ L.toChunks lbs) (\fp -> L.writeFile fp lbs)
|
||||||
|
|
||||||
|
mkFileInfoFile :: Text -> Text -> FilePath -> FileInfo
|
||||||
|
mkFileInfoFile name ct fp = FileInfo name ct (sourceFile fp) (\dst -> runResourceT $ sourceFile fp $$ sinkFile dst)
|
||||||
|
|
||||||
|
data FileUpload = FileUploadMemory (Sink ByteString (ResourceT IO) L.ByteString)
|
||||||
|
| FileUploadDisk (Sink ByteString (ResourceT IO) FilePath)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ postFirstThingR = do
|
|||||||
|
|
||||||
postAfterRunRequestBodyR = do
|
postAfterRunRequestBodyR = do
|
||||||
x <- runRequestBody
|
x <- runRequestBody
|
||||||
_ <- error $ show x
|
_ <- error $ show $ fst x
|
||||||
getHomeR
|
getHomeR
|
||||||
|
|
||||||
errorHandlingTest :: Spec
|
errorHandlingTest :: Spec
|
||||||
|
|||||||
@ -40,19 +40,19 @@ tokenSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqToken)"
|
|||||||
|
|
||||||
noDisabledToken :: Bool
|
noDisabledToken :: Bool
|
||||||
noDisabledToken = reqToken r == Nothing where
|
noDisabledToken = reqToken r == Nothing where
|
||||||
r = parseWaiRequest' defaultRequest [] False g
|
r = parseWaiRequest' defaultRequest [] False 0 g
|
||||||
|
|
||||||
ignoreDisabledToken :: Bool
|
ignoreDisabledToken :: Bool
|
||||||
ignoreDisabledToken = reqToken r == Nothing where
|
ignoreDisabledToken = reqToken r == Nothing where
|
||||||
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] False g
|
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] False 0 g
|
||||||
|
|
||||||
useOldToken :: Bool
|
useOldToken :: Bool
|
||||||
useOldToken = reqToken r == Just "old" where
|
useOldToken = reqToken r == Just "old" where
|
||||||
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True g
|
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True 0 g
|
||||||
|
|
||||||
generateToken :: Bool
|
generateToken :: Bool
|
||||||
generateToken = reqToken r /= Nothing where
|
generateToken = reqToken r /= Nothing where
|
||||||
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True g
|
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True 0 g
|
||||||
|
|
||||||
|
|
||||||
langSpecs :: Spec
|
langSpecs :: Spec
|
||||||
@ -67,21 +67,21 @@ langSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqLangs)"
|
|||||||
respectAcceptLangs :: Bool
|
respectAcceptLangs :: Bool
|
||||||
respectAcceptLangs = reqLangs r == ["en-US", "es", "en"] where
|
respectAcceptLangs = reqLangs r == ["en-US", "es", "en"] where
|
||||||
r = parseWaiRequest' defaultRequest
|
r = parseWaiRequest' defaultRequest
|
||||||
{ requestHeaders = [("Accept-Language", "en-US, es")] } [] False g
|
{ requestHeaders = [("Accept-Language", "en-US, es")] } [] False 0 g
|
||||||
|
|
||||||
respectSessionLang :: Bool
|
respectSessionLang :: Bool
|
||||||
respectSessionLang = reqLangs r == ["en"] where
|
respectSessionLang = reqLangs r == ["en"] where
|
||||||
r = parseWaiRequest' defaultRequest [("_LANG", "en")] False g
|
r = parseWaiRequest' defaultRequest [("_LANG", "en")] False 0 g
|
||||||
|
|
||||||
respectCookieLang :: Bool
|
respectCookieLang :: Bool
|
||||||
respectCookieLang = reqLangs r == ["en"] where
|
respectCookieLang = reqLangs r == ["en"] where
|
||||||
r = parseWaiRequest' defaultRequest
|
r = parseWaiRequest' defaultRequest
|
||||||
{ requestHeaders = [("Cookie", "_LANG=en")]
|
{ requestHeaders = [("Cookie", "_LANG=en")]
|
||||||
} [] False g
|
} [] False 0 g
|
||||||
|
|
||||||
respectQueryLang :: Bool
|
respectQueryLang :: Bool
|
||||||
respectQueryLang = reqLangs r == ["en-US", "en"] where
|
respectQueryLang = reqLangs r == ["en-US", "en"] where
|
||||||
r = parseWaiRequest' defaultRequest { queryString = [("_LANG", Just "en-US")] } [] False g
|
r = parseWaiRequest' defaultRequest { queryString = [("_LANG", Just "en-US")] } [] False 0 g
|
||||||
|
|
||||||
prioritizeLangs :: Bool
|
prioritizeLangs :: Bool
|
||||||
prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "es"] where
|
prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "es"] where
|
||||||
@ -90,7 +90,7 @@ prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "e
|
|||||||
, ("Cookie", "_LANG=en-COOKIE")
|
, ("Cookie", "_LANG=en-COOKIE")
|
||||||
]
|
]
|
||||||
, queryString = [("_LANG", Just "en-QUERY")]
|
, queryString = [("_LANG", Just "en-QUERY")]
|
||||||
} [("_LANG", "en-SESSION")] False g
|
} [("_LANG", "en-SESSION")] False 0 g
|
||||||
|
|
||||||
|
|
||||||
internalRequestTest :: Spec
|
internalRequestTest :: Spec
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user