Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
6d1a25187a
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
*~
|
||||||
*.o
|
*.o
|
||||||
*.o_p
|
*.o_p
|
||||||
*.hi
|
*.hi
|
||||||
@ -6,7 +7,12 @@ dist
|
|||||||
client_session_key.aes
|
client_session_key.aes
|
||||||
cabal-dev/
|
cabal-dev/
|
||||||
yesod/foobar/
|
yesod/foobar/
|
||||||
.virthualenv
|
.hsenv/
|
||||||
|
.cabal-sandbox/
|
||||||
|
cabal.sandbox.config
|
||||||
/vendor/
|
/vendor/
|
||||||
/.shelly/
|
.shelly/
|
||||||
/tarballs/
|
tarballs/
|
||||||
|
*.swp
|
||||||
|
dist
|
||||||
|
client_session_key.aes
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "scripts"]
|
|
||||||
path = scripts
|
|
||||||
url = git://github.com/yesodweb/scripts.git
|
|
||||||
12
.travis.yml
12
.travis.yml
@ -2,12 +2,12 @@ language: haskell
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
- cabal update
|
- cabal update
|
||||||
- cabal install mega-sdist hspec cabal-meta cabal-src
|
- cabal install --force-reinstalls hspec cabal-meta cabal-src alex
|
||||||
- git clone https://github.com/snoyberg/tagstream-conduit.git
|
- cabal-meta install --force-reinstalls
|
||||||
- cd tagstream-conduit
|
|
||||||
- cabal-src-install --src-only
|
|
||||||
- cd ..
|
|
||||||
- cabal-meta install --force-reinstalls --enable-tests
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- echo Done
|
- echo Done
|
||||||
|
- cabal-meta install --enable-tests
|
||||||
|
- mega-sdist --test
|
||||||
|
- cabal install hspec cabal-meta cabal-src
|
||||||
|
- cabal-meta install --force-reinstalls
|
||||||
|
|||||||
15
README
Normal file
15
README
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
Authentication methods for Haskell web applications.
|
||||||
|
|
||||||
|
Note for Rpxnow:
|
||||||
|
By default on some (all?) installs wget does not come with root certificates
|
||||||
|
for SSL. If this is the case then Web.Authenticate.Rpxnow.authenticate will
|
||||||
|
fail as wget cannot establish a secure connection to rpxnow's servers.
|
||||||
|
|
||||||
|
A simple *nix solution, if potentially insecure (man in the middle attacks as
|
||||||
|
you are downloading the certs) is to grab a copy of the certs extracted from
|
||||||
|
those that come with firefox, hosted by CURL at
|
||||||
|
http://curl.haxx.se/ca/cacert.pem , put them somewhere (for ex,
|
||||||
|
~/.wget/cacert.pem) and then edit your ~/.wgetrc to include:
|
||||||
|
ca_certificate=~/.wget/cacert.pem
|
||||||
|
|
||||||
|
This should fix the problem.
|
||||||
40
README.md
40
README.md
@ -9,7 +9,6 @@ An advanced web framework using the Haskell programming language. Featuring:
|
|||||||
* techniques for constant-space memory consumption
|
* techniques for constant-space memory consumption
|
||||||
* asynchronous IO
|
* asynchronous IO
|
||||||
* this is built in to the Haskell programming language (like Erlang)
|
* this is built in to the Haskell programming language (like Erlang)
|
||||||
* handles a greater concurrent load than any other web application server
|
|
||||||
|
|
||||||
# Learn more: http://yesodweb.com/
|
# Learn more: http://yesodweb.com/
|
||||||
|
|
||||||
@ -27,18 +26,19 @@ Your application is a cabal package and you use `cabal` to install its dependenc
|
|||||||
|
|
||||||
Install conflicts are unfortunately common in Haskell development.
|
Install conflicts are unfortunately common in Haskell development.
|
||||||
If you are not using any sandbox tools, you may discover that some of the other haskell installs on your system are broken.
|
If you are not using any sandbox tools, you may discover that some of the other haskell installs on your system are broken.
|
||||||
You can prevent this by using sandbox tools: `cabal-dev` or `hsenv`.
|
You can prevent this by using cabal sandbox.
|
||||||
|
|
||||||
Isolating an entire project with a virtual machine is also a great idea, you just need some tools to help that process.
|
Isolating an entire project is also a great idea, you just need some tools to help that process.
|
||||||
[Vagrant](http://vagrantup.com) is a great tool for that and there is a [Haskell Platform installer](https://bitbucket.org/puffnfresh/vagrant-haskell-heroku) for it.
|
On Linux you can use Docker.
|
||||||
|
On any OS you can use a virtual machine. [Vagrant](http://vagrantup.com) is a great tool for that and there is a [Haskell Platform installer](https://bitbucket.org/puffnfresh/vagrant-haskell-heroku) for it.
|
||||||
|
|
||||||
## Using cabal-dev
|
## Using cabal sandbox
|
||||||
|
|
||||||
cabal-dev creates a sandboxed environment for an individual cabal package.
|
To sandbox a project, type:
|
||||||
Instead of using the `cabal` command, use the `cabal-dev` command which will use the sandbox.
|
|
||||||
|
|
||||||
Use `yesod devel --dev` when developing your application.
|
cabal sandbox init
|
||||||
|
|
||||||
|
This ensures that future installs will be local to the sandboxed directory.
|
||||||
|
|
||||||
|
|
||||||
## Installing the latest development version from github for use with your application
|
## Installing the latest development version from github for use with your application
|
||||||
@ -55,32 +55,18 @@ In your application folder, create a `sources.txt` file with the following conte
|
|||||||
https://github.com/yesodweb/wai
|
https://github.com/yesodweb/wai
|
||||||
|
|
||||||
`./` means build your app. The yesod repos will be cloned and placed in a `vendor` repo.
|
`./` means build your app. The yesod repos will be cloned and placed in a `vendor` repo.
|
||||||
Now run: `cabal-meta install`. If you use `cabal-dev`, run `cabal-meta --dev install`
|
Now run: `cabal-meta install`.
|
||||||
|
|
||||||
This should work almost all of the time. You can read more on [cabal-meta](https://github.com/yesodweb/cabal-meta)
|
This should work almost all of the time. You can read more on [cabal-meta](https://github.com/yesodweb/cabal-meta)
|
||||||
If you aren't building from an application, remove the `./` and create a new directory for your sources.txt first.
|
If you aren't building from an application, remove the `./` and create a new directory for your sources.txt first.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## hsenv (Linux only)
|
## hsenv (Linux and Mac OS X)
|
||||||
|
|
||||||
[hsenv](http://hackage.haskell.org/package/hsenv) prevents your custom build of Yesod from interfering with your currently installed cabal packages:
|
[hsenv](https://github.com/tmhedberg/hsenv) also provides a sandbox, but works at the shell level.
|
||||||
|
Generally we recommend using cabal sandbox, but hsenv has tools for allowing you to use different versions of GHC, which may be useful for you.
|
||||||
|
|
||||||
* hsenv creates an isolated environment like cabal-dev
|
|
||||||
* hsenv works at the shell level, so every shell must activate the hsenv
|
|
||||||
* cabal-dev by default isolates a single cabal package, but hsenv isolates multiple packages together.
|
|
||||||
* cabal-dev can isolate multiple packages together by using the -s sandbox argument
|
|
||||||
|
|
||||||
|
|
||||||
## cabal-src
|
|
||||||
|
|
||||||
The cabal-src tool helps resolve dependency conflicts when installing local packages.
|
|
||||||
This capability is already built in if you are using cabal-dev or cabal-meta. Otherwise install cabal-src with:
|
|
||||||
|
|
||||||
cabal install cabal-src
|
|
||||||
|
|
||||||
Whenever you would use `cabal install` to install a local package, use `cabal-src-install` instead.
|
|
||||||
Our installer script now uses cabal-src-install when it is available.
|
|
||||||
|
|
||||||
|
|
||||||
## Cloning the repos
|
## Cloning the repos
|
||||||
@ -100,7 +86,7 @@ done
|
|||||||
|
|
||||||
## Building your changes to Yesod
|
## Building your changes to Yesod
|
||||||
|
|
||||||
Yesod is composed of 4 "mega-repos", each with multiple cabal packages. `./script/install` will run tests against each package and install each package.
|
The traditional Yesod stack requires 4 "mega-repos", each with multiple cabal packages. `./script/install` will run tests against each package and install each package.
|
||||||
|
|
||||||
### install package in all repos
|
### install package in all repos
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
pkgs=( ./yesod-routes
|
|
||||||
./yesod-core
|
|
||||||
./yesod-json
|
|
||||||
./crypto-conduit
|
|
||||||
./authenticate/authenticate
|
|
||||||
./yesod-static
|
|
||||||
./yesod-persistent
|
|
||||||
./yesod-newsfeed
|
|
||||||
./yesod-form
|
|
||||||
./yesod-auth
|
|
||||||
./yesod-sitemap
|
|
||||||
./yesod-default
|
|
||||||
./yesod )
|
|
||||||
1
scripts
1
scripts
@ -1 +0,0 @@
|
|||||||
Subproject commit 9902ff808afbcb417c6ad125941343878e3afe11
|
|
||||||
@ -9,3 +9,5 @@
|
|||||||
./yesod-test
|
./yesod-test
|
||||||
./yesod-bin
|
./yesod-bin
|
||||||
./yesod
|
./yesod
|
||||||
|
./yesod-eventsource
|
||||||
|
./yesod-websockets
|
||||||
|
|||||||
@ -43,6 +43,7 @@ authOAuth oauth mkCreds = AuthPlugin name dispatch login
|
|||||||
url = PluginR name []
|
url = PluginR name []
|
||||||
lookupTokenSecret = bsToText . fromMaybe "" . lookup "oauth_token_secret" . unCredential
|
lookupTokenSecret = bsToText . fromMaybe "" . lookup "oauth_token_secret" . unCredential
|
||||||
oauthSessionName = "__oauth_token_secret"
|
oauthSessionName = "__oauth_token_secret"
|
||||||
|
|
||||||
dispatch "GET" ["forward"] = do
|
dispatch "GET" ["forward"] = do
|
||||||
render <- lift getUrlRender
|
render <- lift getUrlRender
|
||||||
tm <- getRouteToParent
|
tm <- getRouteToParent
|
||||||
@ -72,8 +73,9 @@ authOAuth oauth mkCreds = AuthPlugin name dispatch login
|
|||||||
master <- getYesod
|
master <- getYesod
|
||||||
accTok <- getAccessToken oauth reqTok (authHttpManager master)
|
accTok <- getAccessToken oauth reqTok (authHttpManager master)
|
||||||
creds <- liftIO $ mkCreds accTok
|
creds <- liftIO $ mkCreds accTok
|
||||||
setCreds True creds
|
setCredsRedirect creds
|
||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
login tm = do
|
login tm = do
|
||||||
render <- getUrlRender
|
render <- getUrlRender
|
||||||
let oaUrl = render $ tm $ oauthUrl name
|
let oaUrl = render $ tm $ oauthUrl name
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
name: yesod-auth-oauth
|
name: yesod-auth-oauth
|
||||||
version: 1.2.0
|
version: 1.3.0
|
||||||
license: BSD3
|
license: BSD3
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Hiromi Ishii
|
author: Hiromi Ishii
|
||||||
maintainer: Hiromi Ishii
|
maintainer: Michael Litchard
|
||||||
synopsis: OAuth Authentication for Yesod.
|
synopsis: OAuth Authentication for Yesod.
|
||||||
category: Web, Yesod
|
category: Web, Yesod
|
||||||
stability: Stable
|
stability: Stable
|
||||||
@ -20,13 +20,13 @@ library
|
|||||||
cpp-options: -DGHC7
|
cpp-options: -DGHC7
|
||||||
else
|
else
|
||||||
build-depends: base >= 4 && < 4.3
|
build-depends: base >= 4 && < 4.3
|
||||||
build-depends: authenticate-oauth >= 1.4 && < 1.5
|
build-depends: authenticate-oauth >= 1.5 && < 1.6
|
||||||
, bytestring >= 0.9.1.4
|
, bytestring >= 0.9.1.4
|
||||||
, yesod-core >= 1.2 && < 1.3
|
, yesod-core >= 1.2 && < 1.3
|
||||||
, yesod-auth >= 1.2 && < 1.3
|
, yesod-auth >= 1.3 && < 1.4
|
||||||
, text >= 0.7 && < 0.12
|
, text >= 0.7
|
||||||
, yesod-form >= 1.3 && < 1.4
|
, yesod-form >= 1.3 && < 1.4
|
||||||
, transformers >= 0.2.2 && < 0.4
|
, transformers >= 0.2.2 && < 0.5
|
||||||
, lifted-base >= 0.2 && < 0.3
|
, lifted-base >= 0.2 && < 0.3
|
||||||
exposed-modules: Yesod.Auth.OAuth
|
exposed-modules: Yesod.Auth.OAuth
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
|||||||
@ -22,6 +22,7 @@ module Yesod.Auth
|
|||||||
-- * Plugin interface
|
-- * Plugin interface
|
||||||
, Creds (..)
|
, Creds (..)
|
||||||
, setCreds
|
, setCreds
|
||||||
|
, setCredsRedirect
|
||||||
, clearCreds
|
, clearCreds
|
||||||
, loginErrorMessage
|
, loginErrorMessage
|
||||||
, loginErrorMessageI
|
, loginErrorMessageI
|
||||||
@ -34,6 +35,11 @@ module Yesod.Auth
|
|||||||
, AuthException (..)
|
, AuthException (..)
|
||||||
-- * Helper
|
-- * Helper
|
||||||
, AuthHandler
|
, AuthHandler
|
||||||
|
-- * Internal
|
||||||
|
, credsKey
|
||||||
|
, provideJsonMessage
|
||||||
|
, messageJson401
|
||||||
|
, asHtml
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad (when)
|
import Control.Monad (when)
|
||||||
@ -62,6 +68,7 @@ import Control.Exception (Exception)
|
|||||||
import Network.HTTP.Types (unauthorized401)
|
import Network.HTTP.Types (unauthorized401)
|
||||||
import Control.Monad.Trans.Resource (MonadResourceBase)
|
import Control.Monad.Trans.Resource (MonadResourceBase)
|
||||||
import qualified Control.Monad.Trans.Writer as Writer
|
import qualified Control.Monad.Trans.Writer as Writer
|
||||||
|
import Control.Monad (void)
|
||||||
|
|
||||||
type AuthRoute = Route Auth
|
type AuthRoute = Route Auth
|
||||||
|
|
||||||
@ -72,7 +79,7 @@ type Piece = Text
|
|||||||
|
|
||||||
data AuthPlugin master = AuthPlugin
|
data AuthPlugin master = AuthPlugin
|
||||||
{ apName :: Text
|
{ apName :: Text
|
||||||
, apDispatch :: Method -> [Piece] -> AuthHandler master ()
|
, apDispatch :: Method -> [Piece] -> AuthHandler master TypedContent
|
||||||
, apLogin :: (Route Auth -> Route master) -> WidgetT master IO ()
|
, apLogin :: (Route Auth -> Route master) -> WidgetT master IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +96,10 @@ data Creds master = Creds
|
|||||||
class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage) => YesodAuth master where
|
class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage) => YesodAuth master where
|
||||||
type AuthId master
|
type AuthId master
|
||||||
|
|
||||||
|
-- | specify the layout. Uses defaultLayout by default
|
||||||
|
authLayout :: WidgetT master IO () -> HandlerT master IO Html
|
||||||
|
authLayout = defaultLayout
|
||||||
|
|
||||||
-- | Default destination on successful login, if no other
|
-- | Default destination on successful login, if no other
|
||||||
-- destination exists.
|
-- destination exists.
|
||||||
loginDest :: master -> Route master
|
loginDest :: master -> Route master
|
||||||
@ -104,10 +115,10 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
|
|||||||
authPlugins :: master -> [AuthPlugin master]
|
authPlugins :: master -> [AuthPlugin master]
|
||||||
|
|
||||||
-- | What to show on the login page.
|
-- | What to show on the login page.
|
||||||
loginHandler :: AuthHandler master RepHtml
|
loginHandler :: AuthHandler master Html
|
||||||
loginHandler = do
|
loginHandler = do
|
||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
lift $ defaultLayout $ do
|
lift $ authLayout $ do
|
||||||
setTitleI Msg.LoginTitle
|
setTitleI Msg.LoginTitle
|
||||||
master <- getYesod
|
master <- getYesod
|
||||||
mapM_ (flip apLogin tp) (authPlugins master)
|
mapM_ (flip apLogin tp) (authPlugins master)
|
||||||
@ -163,6 +174,16 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
|
|||||||
=> HandlerT master IO (Maybe (AuthId master))
|
=> HandlerT master IO (Maybe (AuthId master))
|
||||||
maybeAuthId = defaultMaybeAuthId
|
maybeAuthId = defaultMaybeAuthId
|
||||||
|
|
||||||
|
-- | Called on login error for HTTP requests. By default, calls
|
||||||
|
-- @setMessage@ and redirects to @dest@.
|
||||||
|
onErrorHtml :: (MonadResourceBase m) => Route master -> Text -> HandlerT master m Html
|
||||||
|
onErrorHtml dest msg = do
|
||||||
|
setMessage $ toHtml msg
|
||||||
|
fmap asHtml $ redirect dest
|
||||||
|
|
||||||
|
-- | Internal session key used to hold the authentication information.
|
||||||
|
--
|
||||||
|
-- Since 1.2.3
|
||||||
credsKey :: Text
|
credsKey :: Text
|
||||||
credsKey = "_ID"
|
credsKey = "_ID"
|
||||||
|
|
||||||
@ -212,7 +233,7 @@ cachedAuth aid = runMaybeT $ do
|
|||||||
loginErrorMessageI :: (MonadResourceBase m, YesodAuth master)
|
loginErrorMessageI :: (MonadResourceBase m, YesodAuth master)
|
||||||
=> Route child
|
=> Route child
|
||||||
-> AuthMessage
|
-> AuthMessage
|
||||||
-> HandlerT child (HandlerT master m) a
|
-> HandlerT child (HandlerT master m) TypedContent
|
||||||
loginErrorMessageI dest msg = do
|
loginErrorMessageI dest msg = do
|
||||||
toParent <- getRouteToParent
|
toParent <- getRouteToParent
|
||||||
lift $ loginErrorMessageMasterI (toParent dest) msg
|
lift $ loginErrorMessageMasterI (toParent dest) msg
|
||||||
@ -221,61 +242,74 @@ loginErrorMessageI dest msg = do
|
|||||||
loginErrorMessageMasterI :: (YesodAuth master, MonadResourceBase m, RenderMessage master AuthMessage)
|
loginErrorMessageMasterI :: (YesodAuth master, MonadResourceBase m, RenderMessage master AuthMessage)
|
||||||
=> Route master
|
=> Route master
|
||||||
-> AuthMessage
|
-> AuthMessage
|
||||||
-> HandlerT master m a
|
-> HandlerT master m TypedContent
|
||||||
loginErrorMessageMasterI dest msg = do
|
loginErrorMessageMasterI dest msg = do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
loginErrorMessage dest (mr msg)
|
loginErrorMessage dest (mr msg)
|
||||||
|
|
||||||
-- | For HTML, set the message and redirect to the route.
|
-- | For HTML, set the message and redirect to the route.
|
||||||
-- For JSON, send the message and a 401 status
|
-- For JSON, send the message and a 401 status
|
||||||
loginErrorMessage :: MonadResourceBase m
|
loginErrorMessage :: (YesodAuth master, MonadResourceBase m)
|
||||||
=> Route site
|
=> Route master
|
||||||
-> Text
|
-> Text
|
||||||
-> HandlerT site m a
|
-> HandlerT master m TypedContent
|
||||||
loginErrorMessage dest msg =
|
loginErrorMessage dest msg = messageJson401 msg (onErrorHtml dest msg)
|
||||||
sendResponseStatus unauthorized401 =<< (
|
|
||||||
selectRep $ do
|
messageJson401 :: MonadResourceBase m => Text -> HandlerT master m Html -> HandlerT master m TypedContent
|
||||||
provideRep $ do
|
messageJson401 msg html = selectRep $ do
|
||||||
setMessage $ toHtml msg
|
provideRep html
|
||||||
fmap asHtml $ redirect dest
|
provideRep $ do
|
||||||
provideJsonMessage msg
|
let obj = object ["message" .= msg]
|
||||||
)
|
void $ sendResponseStatus unauthorized401 obj
|
||||||
where
|
return obj
|
||||||
asHtml :: Html -> Html
|
|
||||||
asHtml = id
|
|
||||||
|
|
||||||
provideJsonMessage :: Monad m => Text -> Writer.Writer (Endo [ProvidedRep m]) ()
|
provideJsonMessage :: Monad m => Text -> Writer.Writer (Endo [ProvidedRep m]) ()
|
||||||
provideJsonMessage msg = provideRep $ return $ object ["message" .= msg]
|
provideJsonMessage msg = provideRep $ return $ object ["message" .= msg]
|
||||||
|
|
||||||
|
|
||||||
|
setCredsRedirect :: YesodAuth master
|
||||||
|
=> Creds master -- ^ new credentials
|
||||||
|
-> HandlerT master IO TypedContent
|
||||||
|
setCredsRedirect creds = do
|
||||||
|
y <- getYesod
|
||||||
|
maid <- getAuthId creds
|
||||||
|
case maid of
|
||||||
|
Nothing ->
|
||||||
|
case authRoute y of
|
||||||
|
Nothing -> do
|
||||||
|
messageJson401 "Invalid Login" $ authLayout $
|
||||||
|
toWidget [shamlet|<h1>Invalid login|]
|
||||||
|
Just ar -> loginErrorMessageMasterI ar Msg.InvalidLogin
|
||||||
|
Just aid -> do
|
||||||
|
setSession credsKey $ toPathPiece aid
|
||||||
|
onLogin
|
||||||
|
res <- selectRep $ do
|
||||||
|
provideRepType typeHtml $
|
||||||
|
fmap asHtml $ redirectUltDest $ loginDest y
|
||||||
|
provideJsonMessage "Login Successful"
|
||||||
|
sendResponse res
|
||||||
|
|
||||||
-- | Sets user credentials for the session after checking them with authentication backends.
|
-- | Sets user credentials for the session after checking them with authentication backends.
|
||||||
setCreds :: YesodAuth master
|
setCreds :: YesodAuth master
|
||||||
=> Bool -- ^ if HTTP redirects should be done
|
=> Bool -- ^ if HTTP redirects should be done
|
||||||
-> Creds master -- ^ new credentials
|
-> Creds master -- ^ new credentials
|
||||||
-> HandlerT master IO ()
|
-> HandlerT master IO ()
|
||||||
setCreds doRedirects creds = do
|
setCreds doRedirects creds =
|
||||||
y <- getYesod
|
if doRedirects
|
||||||
maid <- getAuthId creds
|
then void $ setCredsRedirect creds
|
||||||
case maid of
|
else do maid <- getAuthId creds
|
||||||
Nothing -> when doRedirects $ do
|
case maid of
|
||||||
case authRoute y of
|
Nothing -> return ()
|
||||||
Nothing -> do
|
Just aid -> setSession credsKey $ toPathPiece aid
|
||||||
sendResponseStatus unauthorized401 =<< (
|
|
||||||
selectRep $ do
|
-- | same as defaultLayoutJson, but uses authLayout
|
||||||
provideRep $ defaultLayout $ toWidget [shamlet|<h1>Invalid login|]
|
authLayoutJson :: (YesodAuth site, ToJSON j)
|
||||||
provideJsonMessage "Invalid Login"
|
=> WidgetT site IO () -- ^ HTML
|
||||||
)
|
-> HandlerT site IO j -- ^ JSON
|
||||||
Just ar -> loginErrorMessageMasterI ar Msg.InvalidLogin
|
-> HandlerT site IO TypedContent
|
||||||
Just aid -> do
|
authLayoutJson w json = selectRep $ do
|
||||||
setSession credsKey $ toPathPiece aid
|
provideRep $ authLayout w
|
||||||
when doRedirects $ do
|
provideRep $ fmap toJSON json
|
||||||
onLogin
|
|
||||||
res <- selectRep $ do
|
|
||||||
provideRepType typeHtml $ do
|
|
||||||
_ <- redirectUltDest $ loginDest y
|
|
||||||
return ()
|
|
||||||
provideJsonMessage "Login Successful"
|
|
||||||
sendResponse res
|
|
||||||
|
|
||||||
-- | Clears current user credentials for the session.
|
-- | Clears current user credentials for the session.
|
||||||
--
|
--
|
||||||
@ -293,7 +327,7 @@ clearCreds doRedirects = do
|
|||||||
getCheckR :: AuthHandler master TypedContent
|
getCheckR :: AuthHandler master TypedContent
|
||||||
getCheckR = lift $ do
|
getCheckR = lift $ do
|
||||||
creds <- maybeAuthId
|
creds <- maybeAuthId
|
||||||
defaultLayoutJson (do
|
authLayoutJson (do
|
||||||
setTitle "Authentication Status"
|
setTitle "Authentication Status"
|
||||||
toWidget $ html' creds) (return $ jsonCreds creds)
|
toWidget $ html' creds) (return $ jsonCreds creds)
|
||||||
where
|
where
|
||||||
@ -316,7 +350,7 @@ setUltDestReferer' = lift $ do
|
|||||||
master <- getYesod
|
master <- getYesod
|
||||||
when (redirectToReferer master) setUltDestReferer
|
when (redirectToReferer master) setUltDestReferer
|
||||||
|
|
||||||
getLoginR :: AuthHandler master RepHtml
|
getLoginR :: AuthHandler master Html
|
||||||
getLoginR = setUltDestReferer' >> loginHandler
|
getLoginR = setUltDestReferer' >> loginHandler
|
||||||
|
|
||||||
getLogoutR :: AuthHandler master ()
|
getLogoutR :: AuthHandler master ()
|
||||||
@ -325,7 +359,7 @@ getLogoutR = setUltDestReferer' >> redirectToPost LogoutR
|
|||||||
postLogoutR :: AuthHandler master ()
|
postLogoutR :: AuthHandler master ()
|
||||||
postLogoutR = lift $ clearCreds True
|
postLogoutR = lift $ clearCreds True
|
||||||
|
|
||||||
handlePluginR :: Text -> [Text] -> AuthHandler master ()
|
handlePluginR :: Text -> [Text] -> AuthHandler master TypedContent
|
||||||
handlePluginR plugin pieces = do
|
handlePluginR plugin pieces = do
|
||||||
master <- lift getYesod
|
master <- lift getYesod
|
||||||
env <- waiRequest
|
env <- waiRequest
|
||||||
@ -334,6 +368,11 @@ handlePluginR plugin pieces = do
|
|||||||
[] -> notFound
|
[] -> notFound
|
||||||
ap:_ -> apDispatch ap method pieces
|
ap:_ -> apDispatch ap method pieces
|
||||||
|
|
||||||
|
-- | Similar to 'maybeAuthId', but additionally look up the value associated
|
||||||
|
-- with the user\'s database identifier to get the value in the database. This
|
||||||
|
-- assumes that you are using a Persistent database.
|
||||||
|
--
|
||||||
|
-- Since 1.1.0
|
||||||
maybeAuth :: ( YesodAuth master
|
maybeAuth :: ( YesodAuth master
|
||||||
, PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val
|
, PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val
|
||||||
, b ~ YesodPersistBackend master
|
, b ~ YesodPersistBackend master
|
||||||
@ -383,6 +422,10 @@ type AuthEntity master = KeyEntity (AuthId master)
|
|||||||
requireAuthId :: YesodAuthPersist master => HandlerT master IO (AuthId master)
|
requireAuthId :: YesodAuthPersist master => HandlerT master IO (AuthId master)
|
||||||
requireAuthId = maybeAuthId >>= maybe redirectLogin return
|
requireAuthId = maybeAuthId >>= maybe redirectLogin return
|
||||||
|
|
||||||
|
-- | Similar to 'maybeAuth', but redirects to a login page if user is not
|
||||||
|
-- authenticated.
|
||||||
|
--
|
||||||
|
-- Since 1.1.0
|
||||||
requireAuth :: YesodAuthPersist master => HandlerT master IO (Entity (AuthEntity master))
|
requireAuth :: YesodAuthPersist master => HandlerT master IO (Entity (AuthEntity master))
|
||||||
requireAuth = maybeAuth >>= maybe redirectLogin return
|
requireAuth = maybeAuth >>= maybe redirectLogin return
|
||||||
|
|
||||||
@ -403,3 +446,6 @@ instance Exception AuthException
|
|||||||
|
|
||||||
instance YesodAuth master => YesodSubDispatch Auth (HandlerT master IO) where
|
instance YesodAuth master => YesodSubDispatch Auth (HandlerT master IO) where
|
||||||
yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth)
|
yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth)
|
||||||
|
|
||||||
|
asHtml :: Html -> Html
|
||||||
|
asHtml = id
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module Yesod.Auth.BrowserId
|
module Yesod.Auth.BrowserId
|
||||||
( authBrowserId
|
( authBrowserId
|
||||||
, createOnClick
|
, createOnClick, createOnClickOverride
|
||||||
, def
|
, def
|
||||||
, BrowserIdSettings
|
, BrowserIdSettings
|
||||||
, bisAudience
|
, bisAudience
|
||||||
@ -75,8 +75,9 @@ authBrowserId bis@BrowserIdSettings {..} = AuthPlugin
|
|||||||
case memail of
|
case memail of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
$logErrorS "yesod-auth" "BrowserID assertion failure"
|
$logErrorS "yesod-auth" "BrowserID assertion failure"
|
||||||
loginErrorMessage LoginR "BrowserID login error."
|
tm <- getRouteToParent
|
||||||
Just email -> lift $ setCreds True Creds
|
lift $ loginErrorMessage (tm LoginR) "BrowserID login error."
|
||||||
|
Just email -> lift $ setCredsRedirect Creds
|
||||||
{ credsPlugin = pid
|
{ credsPlugin = pid
|
||||||
, credsIdent = email
|
, credsIdent = email
|
||||||
, credsExtra = []
|
, credsExtra = []
|
||||||
@ -106,14 +107,16 @@ $newline never
|
|||||||
|
|
||||||
-- | Generates a function to handle on-click events, and returns that function
|
-- | Generates a function to handle on-click events, and returns that function
|
||||||
-- name.
|
-- name.
|
||||||
createOnClick :: BrowserIdSettings
|
createOnClickOverride :: BrowserIdSettings
|
||||||
-> (Route Auth -> Route master)
|
-> (Route Auth -> Route master)
|
||||||
|
-> Maybe (Route master)
|
||||||
-> WidgetT master IO Text
|
-> WidgetT master IO Text
|
||||||
createOnClick BrowserIdSettings {..} toMaster = do
|
createOnClickOverride BrowserIdSettings {..} toMaster mOnRegistration = do
|
||||||
unless bisLazyLoad $ addScriptRemote browserIdJs
|
unless bisLazyLoad $ addScriptRemote browserIdJs
|
||||||
onclick <- newIdent
|
onclick <- newIdent
|
||||||
render <- getUrlRender
|
render <- getUrlRender
|
||||||
let login = toJSON $ getPath $ render (toMaster LoginR)
|
let login = toJSON $ getPath $ render loginRoute -- (toMaster LoginR)
|
||||||
|
loginRoute = maybe (toMaster LoginR) id mOnRegistration
|
||||||
toWidget [julius|
|
toWidget [julius|
|
||||||
function #{rawJS onclick}() {
|
function #{rawJS onclick}() {
|
||||||
if (navigator.id) {
|
if (navigator.id) {
|
||||||
@ -151,3 +154,10 @@ createOnClick BrowserIdSettings {..} toMaster = do
|
|||||||
getPath t = fromMaybe t $ do
|
getPath t = fromMaybe t $ do
|
||||||
uri <- parseURI $ T.unpack t
|
uri <- parseURI $ T.unpack t
|
||||||
return $ T.pack $ uriPath uri
|
return $ T.pack $ uriPath uri
|
||||||
|
|
||||||
|
-- | Generates a function to handle on-click events, and returns that function
|
||||||
|
-- name.
|
||||||
|
createOnClick :: BrowserIdSettings
|
||||||
|
-> (Route Auth -> Route master)
|
||||||
|
-> WidgetT master IO Text
|
||||||
|
createOnClick bidSettings toMaster = createOnClickOverride bidSettings toMaster Nothing
|
||||||
|
|||||||
@ -18,7 +18,7 @@ authDummy =
|
|||||||
where
|
where
|
||||||
dispatch "POST" [] = do
|
dispatch "POST" [] = do
|
||||||
ident <- lift $ runInputPost $ ireq textField "ident"
|
ident <- lift $ runInputPost $ ireq textField "ident"
|
||||||
lift $ setCreds True $ Creds "dummy" ident []
|
lift $ setCredsRedirect $ Creds "dummy" ident []
|
||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
url = PluginR "dummy" []
|
url = PluginR "dummy" []
|
||||||
login authToMaster =
|
login authToMaster =
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE PatternGuards #-}
|
{-# LANGUAGE PatternGuards #-}
|
||||||
|
{-# LANGUAGE Rank2Types #-}
|
||||||
module Yesod.Auth.Email
|
module Yesod.Auth.Email
|
||||||
( -- * Plugin
|
( -- * Plugin
|
||||||
authEmail
|
authEmail
|
||||||
@ -24,25 +25,32 @@ module Yesod.Auth.Email
|
|||||||
-- * Misc
|
-- * Misc
|
||||||
, loginLinkKey
|
, loginLinkKey
|
||||||
, setLoginLinkKey
|
, setLoginLinkKey
|
||||||
|
-- * Default handlers
|
||||||
|
, defaultRegisterHandler
|
||||||
|
, defaultForgotPasswordHandler
|
||||||
|
, defaultSetPasswordHandler
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Network.Mail.Mime (randomString)
|
import Network.Mail.Mime (randomString)
|
||||||
import Yesod.Auth
|
import Yesod.Auth
|
||||||
import System.Random
|
import System.Random
|
||||||
import Data.Digest.Pure.MD5
|
|
||||||
import qualified Data.Text as TS
|
import qualified Data.Text as TS
|
||||||
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.Encoding as TE
|
||||||
import qualified Data.Text.Lazy.Encoding as TLE
|
import qualified Data.Text.Lazy.Encoding as TLE
|
||||||
|
import qualified Crypto.Hash.MD5 as H
|
||||||
|
import Data.ByteString.Base16 as B16
|
||||||
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
||||||
import Data.Text.Encoding.Error (lenientDecode)
|
import Data.Text.Encoding.Error (lenientDecode)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import qualified Crypto.PasswordStore as PS
|
import qualified Yesod.PasswordStore as PS
|
||||||
import qualified Text.Email.Validate
|
import qualified Text.Email.Validate
|
||||||
import qualified Yesod.Auth.Message as Msg
|
import qualified Yesod.Auth.Message as Msg
|
||||||
import Control.Applicative ((<$>), (<*>))
|
import Control.Applicative ((<$>), (<*>))
|
||||||
|
import Control.Monad (void)
|
||||||
import Yesod.Form
|
import Yesod.Form
|
||||||
import Control.Monad (when)
|
|
||||||
import Data.Time (getCurrentTime, addUTCTime)
|
import Data.Time (getCurrentTime, addUTCTime)
|
||||||
import Safe (readMay)
|
import Safe (readMay)
|
||||||
|
|
||||||
@ -78,7 +86,11 @@ data EmailCreds site = EmailCreds
|
|||||||
, emailCredsEmail :: Email
|
, emailCredsEmail :: Email
|
||||||
}
|
}
|
||||||
|
|
||||||
class (YesodAuth site, PathPiece (AuthEmailId site)) => YesodAuthEmail site where
|
class ( YesodAuth site
|
||||||
|
, PathPiece (AuthEmailId site)
|
||||||
|
, (RenderMessage site Msg.AuthMessage)
|
||||||
|
)
|
||||||
|
=> YesodAuthEmail site where
|
||||||
type AuthEmailId site
|
type AuthEmailId site
|
||||||
|
|
||||||
-- | Add a new email address to the database, but indicate that the address
|
-- | Add a new email address to the database, but indicate that the address
|
||||||
@ -164,6 +176,63 @@ class (YesodAuth site, PathPiece (AuthEmailId site)) => YesodAuthEmail site wher
|
|||||||
| TS.length x >= 3 = return $ Right ()
|
| TS.length x >= 3 = return $ Right ()
|
||||||
| otherwise = return $ Left "Password must be at least three characters"
|
| otherwise = return $ Left "Password must be at least three characters"
|
||||||
|
|
||||||
|
-- | Response after sending a confirmation email.
|
||||||
|
--
|
||||||
|
-- Since 1.2.2
|
||||||
|
confirmationEmailSentResponse :: Text -> HandlerT site IO TypedContent
|
||||||
|
confirmationEmailSentResponse identifier = do
|
||||||
|
mr <- getMessageRender
|
||||||
|
messageJson401 (mr msg) $ authLayout $ do
|
||||||
|
setTitleI Msg.ConfirmationEmailSentTitle
|
||||||
|
[whamlet|<p>_{msg}|]
|
||||||
|
where
|
||||||
|
msg = Msg.ConfirmationEmailSent identifier
|
||||||
|
|
||||||
|
-- | Additional normalization of email addresses, besides standard canonicalization.
|
||||||
|
--
|
||||||
|
-- Default: Lower case the email address.
|
||||||
|
--
|
||||||
|
-- Since 1.2.3
|
||||||
|
normalizeEmailAddress :: site -> Text -> Text
|
||||||
|
normalizeEmailAddress _ = TS.toLower
|
||||||
|
|
||||||
|
-- | Handler called to render the registration page. The
|
||||||
|
-- default works fine, but you may want to override it in
|
||||||
|
-- order to have a different DOM.
|
||||||
|
--
|
||||||
|
-- Default: 'defaultRegisterHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6.
|
||||||
|
registerHandler :: AuthHandler site Html
|
||||||
|
registerHandler = defaultRegisterHandler
|
||||||
|
|
||||||
|
-- | Handler called to render the \"forgot password\" page.
|
||||||
|
-- The default works fine, but you may want to override it in
|
||||||
|
-- order to have a different DOM.
|
||||||
|
--
|
||||||
|
-- Default: 'defaultForgotPasswordHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6.
|
||||||
|
forgotPasswordHandler :: AuthHandler site Html
|
||||||
|
forgotPasswordHandler = defaultForgotPasswordHandler
|
||||||
|
|
||||||
|
-- | Handler called to render the \"set password\" page. The
|
||||||
|
-- default works fine, but you may want to override it in
|
||||||
|
-- order to have a different DOM.
|
||||||
|
--
|
||||||
|
-- Default: 'defaultSetPasswordHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6.
|
||||||
|
setPasswordHandler ::
|
||||||
|
Bool
|
||||||
|
-- ^ Whether the old password is needed. If @True@, a
|
||||||
|
-- field for the old password should be presented.
|
||||||
|
-- Otherwise, just two fields for the new password are
|
||||||
|
-- needed.
|
||||||
|
-> AuthHandler site TypedContent
|
||||||
|
setPasswordHandler = defaultSetPasswordHandler
|
||||||
|
|
||||||
|
|
||||||
authEmail :: YesodAuthEmail m => AuthPlugin m
|
authEmail :: YesodAuthEmail m => AuthPlugin m
|
||||||
authEmail =
|
authEmail =
|
||||||
AuthPlugin "email" dispatch $ \tm ->
|
AuthPlugin "email" dispatch $ \tm ->
|
||||||
@ -181,8 +250,11 @@ $newline never
|
|||||||
<input type="password" name="password">
|
<input type="password" name="password">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<input type="submit" value=_{Msg.LoginViaEmail}>
|
<button type=submit .btn .btn-success>
|
||||||
<a href="@{tm registerR}">I don't have an account
|
_{Msg.LoginViaEmail}
|
||||||
|
|
||||||
|
<a href="@{tm registerR}" .btn .btn-default>
|
||||||
|
_{Msg.RegisterLong}
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
dispatch "GET" ["register"] = getRegisterR >>= sendResponse
|
dispatch "GET" ["register"] = getRegisterR >>= sendResponse
|
||||||
@ -199,10 +271,16 @@ $newline never
|
|||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
getRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
getRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
||||||
getRegisterR = do
|
getRegisterR = registerHandler
|
||||||
|
|
||||||
|
-- | Default implementation of 'registerHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6
|
||||||
|
defaultRegisterHandler :: YesodAuthEmail master => AuthHandler master Html
|
||||||
|
defaultRegisterHandler = do
|
||||||
email <- newIdent
|
email <- newIdent
|
||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
lift $ defaultLayout $ do
|
lift $ authLayout $ do
|
||||||
setTitleI Msg.RegisterLong
|
setTitleI Msg.RegisterLong
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<p>_{Msg.EnterEmail}
|
<p>_{Msg.EnterEmail}
|
||||||
@ -216,51 +294,59 @@ getRegisterR = do
|
|||||||
registerHelper :: YesodAuthEmail master
|
registerHelper :: YesodAuthEmail master
|
||||||
=> Bool -- ^ allow usernames?
|
=> Bool -- ^ allow usernames?
|
||||||
-> Route Auth
|
-> Route Auth
|
||||||
-> HandlerT Auth (HandlerT master IO) Html
|
-> HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
registerHelper allowUsername dest = do
|
registerHelper allowUsername dest = do
|
||||||
y <- lift getYesod
|
y <- lift getYesod
|
||||||
midentifier <- lookupPostParam "email"
|
midentifier <- lookupPostParam "email"
|
||||||
identifier <-
|
let eidentifier = case midentifier of
|
||||||
case midentifier of
|
Nothing -> Left Msg.NoIdentifierProvided
|
||||||
Nothing -> do
|
|
||||||
loginErrorMessageI dest Msg.NoIdentifierProvided
|
|
||||||
Just x
|
Just x
|
||||||
| Just x' <- Text.Email.Validate.canonicalizeEmail (encodeUtf8 x) ->
|
| Just x' <- Text.Email.Validate.canonicalizeEmail (encodeUtf8 x) ->
|
||||||
return $ decodeUtf8With lenientDecode x'
|
Right $ normalizeEmailAddress y $ decodeUtf8With lenientDecode x'
|
||||||
| allowUsername -> return $ TS.strip x
|
| allowUsername -> Right $ TS.strip x
|
||||||
| otherwise -> do
|
| otherwise -> Left Msg.InvalidEmailAddress
|
||||||
loginErrorMessageI dest Msg.InvalidEmailAddress
|
|
||||||
mecreds <- lift $ getEmailCreds identifier
|
|
||||||
(lid, verKey, email) <-
|
|
||||||
case mecreds of
|
|
||||||
Just (EmailCreds lid _ _ (Just key) email) -> return (lid, key, email)
|
|
||||||
Just (EmailCreds lid _ _ Nothing email) -> do
|
|
||||||
key <- liftIO $ randomKey y
|
|
||||||
lift $ setVerifyKey lid key
|
|
||||||
return (lid, key, email)
|
|
||||||
Nothing
|
|
||||||
| allowUsername -> do
|
|
||||||
setMessage $ toHtml $ "No record for that identifier in our database: " `TS.append` identifier
|
|
||||||
redirect dest
|
|
||||||
| otherwise -> do
|
|
||||||
key <- liftIO $ randomKey y
|
|
||||||
lid <- lift $ addUnverified identifier key
|
|
||||||
return (lid, key, identifier)
|
|
||||||
render <- getUrlRender
|
|
||||||
let verUrl = render $ verify (toPathPiece lid) verKey
|
|
||||||
lift $ sendVerifyEmail email verKey verUrl
|
|
||||||
lift $ defaultLayout $ do
|
|
||||||
setTitleI Msg.ConfirmationEmailSentTitle
|
|
||||||
[whamlet|<p>_{Msg.ConfirmationEmailSent identifier}|]
|
|
||||||
|
|
||||||
postRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
case eidentifier of
|
||||||
|
Left route -> loginErrorMessageI dest route
|
||||||
|
Right identifier -> do
|
||||||
|
|
||||||
|
mecreds <- lift $ getEmailCreds identifier
|
||||||
|
registerCreds <-
|
||||||
|
case mecreds of
|
||||||
|
Just (EmailCreds lid _ _ (Just key) email) -> return $ Just (lid, key, email)
|
||||||
|
Just (EmailCreds lid _ _ Nothing email) -> do
|
||||||
|
key <- liftIO $ randomKey y
|
||||||
|
lift $ setVerifyKey lid key
|
||||||
|
return $ Just (lid, key, email)
|
||||||
|
Nothing
|
||||||
|
| allowUsername -> return Nothing
|
||||||
|
| otherwise -> do
|
||||||
|
key <- liftIO $ randomKey y
|
||||||
|
lid <- lift $ addUnverified identifier key
|
||||||
|
return $ Just (lid, key, identifier)
|
||||||
|
|
||||||
|
case registerCreds of
|
||||||
|
Nothing -> loginErrorMessageI dest (Msg.IdentifierNotFound identifier)
|
||||||
|
Just (lid, verKey, email) -> do
|
||||||
|
render <- getUrlRender
|
||||||
|
let verUrl = render $ verify (toPathPiece lid) verKey
|
||||||
|
lift $ sendVerifyEmail email verKey verUrl
|
||||||
|
lift $ confirmationEmailSentResponse identifier
|
||||||
|
|
||||||
|
postRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
postRegisterR = registerHelper False registerR
|
postRegisterR = registerHelper False registerR
|
||||||
|
|
||||||
getForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
getForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
||||||
getForgotPasswordR = do
|
getForgotPasswordR = forgotPasswordHandler
|
||||||
|
|
||||||
|
-- | Default implementation of 'forgotPasswordHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6
|
||||||
|
defaultForgotPasswordHandler :: YesodAuthEmail master => AuthHandler master Html
|
||||||
|
defaultForgotPasswordHandler = do
|
||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
email <- newIdent
|
email <- newIdent
|
||||||
lift $ defaultLayout $ do
|
lift $ authLayout $ do
|
||||||
setTitleI Msg.PasswordResetTitle
|
setTitleI Msg.PasswordResetTitle
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<p>_{Msg.PasswordResetPrompt}
|
<p>_{Msg.PasswordResetPrompt}
|
||||||
@ -271,35 +357,43 @@ getForgotPasswordR = do
|
|||||||
<button .btn>_{Msg.SendPasswordResetEmail}
|
<button .btn>_{Msg.SendPasswordResetEmail}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
postForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
postForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
postForgotPasswordR = registerHelper True forgotPasswordR
|
postForgotPasswordR = registerHelper True forgotPasswordR
|
||||||
|
|
||||||
getVerifyR :: YesodAuthEmail site
|
getVerifyR :: YesodAuthEmail site
|
||||||
=> AuthEmailId site
|
=> AuthEmailId site
|
||||||
-> Text
|
-> Text
|
||||||
-> HandlerT Auth (HandlerT site IO) Html
|
-> HandlerT Auth (HandlerT site IO) TypedContent
|
||||||
getVerifyR lid key = do
|
getVerifyR lid key = do
|
||||||
realKey <- lift $ getVerifyKey lid
|
realKey <- lift $ getVerifyKey lid
|
||||||
memail <- lift $ getEmail lid
|
memail <- lift $ getEmail lid
|
||||||
|
mr <- lift getMessageRender
|
||||||
case (realKey == Just key, memail) of
|
case (realKey == Just key, memail) of
|
||||||
(True, Just email) -> do
|
(True, Just email) -> do
|
||||||
muid <- lift $ verifyAccount lid
|
muid <- lift $ verifyAccount lid
|
||||||
case muid of
|
case muid of
|
||||||
Nothing -> return ()
|
Nothing -> invalidKey mr
|
||||||
Just uid -> do
|
Just uid -> do
|
||||||
lift $ setCreds False $ Creds "email-verify" email [("verifiedEmail", email)] -- FIXME uid?
|
lift $ setCreds False $ Creds "email-verify" email [("verifiedEmail", email)] -- FIXME uid?
|
||||||
lift $ setMessageI Msg.AddressVerified
|
|
||||||
lift $ setLoginLinkKey uid
|
lift $ setLoginLinkKey uid
|
||||||
redirect setpassR
|
let msgAv = Msg.AddressVerified
|
||||||
_ -> return ()
|
selectRep $ do
|
||||||
lift $ defaultLayout $ do
|
provideRep $ do
|
||||||
setTitleI Msg.InvalidKey
|
lift $ setMessageI msgAv
|
||||||
|
fmap asHtml $ redirect setpassR
|
||||||
|
provideJsonMessage $ mr msgAv
|
||||||
|
_ -> invalidKey mr
|
||||||
|
where
|
||||||
|
msgIk = Msg.InvalidKey
|
||||||
|
invalidKey mr = messageJson401 (mr msgIk) $ lift $ authLayout $ do
|
||||||
|
setTitleI msgIk
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<p>_{Msg.InvalidKey}
|
<p>_{msgIk}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
postLoginR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) ()
|
|
||||||
|
postLoginR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
postLoginR = do
|
postLoginR = do
|
||||||
(identifier, pass) <- lift $ runInputPost $ (,)
|
(identifier, pass) <- lift $ runInputPost $ (,)
|
||||||
<$> ireq textField "email"
|
<$> ireq textField "email"
|
||||||
@ -322,30 +416,40 @@ postLoginR = do
|
|||||||
let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier
|
let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier
|
||||||
case maid of
|
case maid of
|
||||||
Just email ->
|
Just email ->
|
||||||
lift $ setCreds True $ Creds
|
lift $ setCredsRedirect $ Creds
|
||||||
(if isEmail then "email" else "username")
|
(if isEmail then "email" else "username")
|
||||||
email
|
email
|
||||||
[("verifiedEmail", email)]
|
[("verifiedEmail", email)]
|
||||||
Nothing -> do
|
Nothing ->
|
||||||
loginErrorMessageI LoginR $
|
loginErrorMessageI LoginR $
|
||||||
if isEmail
|
if isEmail
|
||||||
then Msg.InvalidEmailPass
|
then Msg.InvalidEmailPass
|
||||||
else Msg.InvalidUsernamePass
|
else Msg.InvalidUsernamePass
|
||||||
|
|
||||||
getPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html
|
getPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
getPasswordR = do
|
getPasswordR = do
|
||||||
maid <- lift maybeAuthId
|
maid <- lift maybeAuthId
|
||||||
|
case maid of
|
||||||
|
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
|
||||||
|
Just _ -> do
|
||||||
|
needOld <- maybe (return True) (lift . needOldPassword) maid
|
||||||
|
setPasswordHandler needOld
|
||||||
|
|
||||||
|
-- | Default implementation of 'setPasswordHandler'.
|
||||||
|
--
|
||||||
|
-- Since: 1.2.6
|
||||||
|
defaultSetPasswordHandler :: YesodAuthEmail master => Bool -> AuthHandler master TypedContent
|
||||||
|
defaultSetPasswordHandler needOld = do
|
||||||
|
tp <- getRouteToParent
|
||||||
pass0 <- newIdent
|
pass0 <- newIdent
|
||||||
pass1 <- newIdent
|
pass1 <- newIdent
|
||||||
pass2 <- newIdent
|
pass2 <- newIdent
|
||||||
case maid of
|
mr <- lift getMessageRender
|
||||||
Just _ -> return ()
|
selectRep $ do
|
||||||
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
|
provideJsonMessage $ mr Msg.SetPass
|
||||||
tp <- getRouteToParent
|
provideRep $ lift $ authLayout $ do
|
||||||
needOld <- maybe (return True) (lift . needOldPassword) maid
|
setTitleI Msg.SetPassTitle
|
||||||
lift $ defaultLayout $ do
|
[whamlet|
|
||||||
setTitleI Msg.SetPassTitle
|
|
||||||
[whamlet|
|
|
||||||
$newline never
|
$newline never
|
||||||
<h3>_{Msg.SetPass}
|
<h3>_{Msg.SetPass}
|
||||||
<form method="post" action="@{tp setpassR}">
|
<form method="post" action="@{tp setpassR}">
|
||||||
@ -371,41 +475,52 @@ $newline never
|
|||||||
<input type="submit" value=_{Msg.SetPassTitle}>
|
<input type="submit" value=_{Msg.SetPassTitle}>
|
||||||
|]
|
|]
|
||||||
|
|
||||||
postPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) ()
|
postPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
|
||||||
postPasswordR = do
|
postPasswordR = do
|
||||||
maid <- lift maybeAuthId
|
maid <- lift maybeAuthId
|
||||||
aid <- case maid of
|
case maid of
|
||||||
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
|
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
|
||||||
Just aid -> return aid
|
Just aid -> do
|
||||||
|
tm <- getRouteToParent
|
||||||
|
|
||||||
needOld <- lift $ needOldPassword aid
|
needOld <- lift $ needOldPassword aid
|
||||||
when needOld $ do
|
if not needOld then confirmPassword aid tm else do
|
||||||
current <- lift $ runInputPost $ ireq textField "current"
|
current <- lift $ runInputPost $ ireq textField "current"
|
||||||
mrealpass <- lift $ getPassword aid
|
mrealpass <- lift $ getPassword aid
|
||||||
case mrealpass of
|
case mrealpass of
|
||||||
Nothing -> loginErrorMessage setpassR "You do not currently have a password set on your account"
|
Nothing ->
|
||||||
Just realpass
|
lift $ loginErrorMessage (tm setpassR) "You do not currently have a password set on your account"
|
||||||
| isValidPass current realpass -> return ()
|
Just realpass
|
||||||
| otherwise -> loginErrorMessage setpassR "Invalid current password, please try again"
|
| isValidPass current realpass -> confirmPassword aid tm
|
||||||
|
| otherwise ->
|
||||||
|
lift $ loginErrorMessage (tm setpassR) "Invalid current password, please try again"
|
||||||
|
|
||||||
(new, confirm) <- lift $ runInputPost $ (,)
|
where
|
||||||
<$> ireq textField "new"
|
msgOk = Msg.PassUpdated
|
||||||
<*> ireq textField "confirm"
|
confirmPassword aid tm = do
|
||||||
when (new /= confirm) $
|
(new, confirm) <- lift $ runInputPost $ (,)
|
||||||
loginErrorMessageI setpassR Msg.PassMismatch
|
<$> ireq textField "new"
|
||||||
|
<*> ireq textField "confirm"
|
||||||
|
|
||||||
isSecure <- lift $ checkPasswordSecurity aid new
|
if new /= confirm
|
||||||
case isSecure of
|
then loginErrorMessageI setpassR Msg.PassMismatch
|
||||||
Left e -> loginErrorMessage setpassR e
|
else do
|
||||||
Right () -> return ()
|
isSecure <- lift $ checkPasswordSecurity aid new
|
||||||
|
case isSecure of
|
||||||
|
Left e -> lift $ loginErrorMessage (tm setpassR) e
|
||||||
|
Right () -> do
|
||||||
|
salted <- liftIO $ saltPass new
|
||||||
|
y <- lift $ do
|
||||||
|
setPassword aid salted
|
||||||
|
deleteSession loginLinkKey
|
||||||
|
setMessageI msgOk
|
||||||
|
getYesod
|
||||||
|
|
||||||
salted <- liftIO $ saltPass new
|
mr <- lift getMessageRender
|
||||||
lift $ do
|
selectRep $ do
|
||||||
y <- getYesod
|
provideRep $
|
||||||
setPassword aid salted
|
fmap asHtml $ lift $ redirect $ afterPasswordRoute y
|
||||||
setMessageI Msg.PassUpdated
|
provideJsonMessage (mr msgOk)
|
||||||
deleteSession loginLinkKey
|
|
||||||
redirect $ afterPasswordRoute y
|
|
||||||
|
|
||||||
saltLength :: Int
|
saltLength :: Int
|
||||||
saltLength = 5
|
saltLength = 5
|
||||||
@ -413,11 +528,12 @@ saltLength = 5
|
|||||||
-- | Salt a password with a randomly generated salt.
|
-- | Salt a password with a randomly generated salt.
|
||||||
saltPass :: Text -> IO Text
|
saltPass :: Text -> IO Text
|
||||||
saltPass = fmap (decodeUtf8With lenientDecode)
|
saltPass = fmap (decodeUtf8With lenientDecode)
|
||||||
. flip PS.makePassword 12
|
. flip PS.makePassword 14
|
||||||
. encodeUtf8
|
. encodeUtf8
|
||||||
|
|
||||||
saltPass' :: String -> String -> String
|
saltPass' :: String -> String -> String
|
||||||
saltPass' salt pass = salt ++ show (md5 $ TLE.encodeUtf8 $ TL.pack $ salt ++ pass)
|
saltPass' salt pass =
|
||||||
|
salt ++ T.unpack (TE.decodeUtf8 $ B16.encode $ H.hash $ TE.encodeUtf8 $ T.pack $ salt ++ pass)
|
||||||
|
|
||||||
isValidPass :: Text -- ^ cleartext password
|
isValidPass :: Text -- ^ cleartext password
|
||||||
-> SaltedPass -- ^ salted password
|
-> SaltedPass -- ^ salted password
|
||||||
|
|||||||
@ -54,7 +54,9 @@ authGoogleEmail =
|
|||||||
, ("openid.ui.icon", "true")
|
, ("openid.ui.icon", "true")
|
||||||
] (authHttpManager master)
|
] (authHttpManager master)
|
||||||
either
|
either
|
||||||
(\err -> loginErrorMessage LoginR $ T.pack $ show (err :: SomeException))
|
(\err -> do
|
||||||
|
tm <- getRouteToParent
|
||||||
|
lift $ loginErrorMessage (tm LoginR) $ T.pack $ show (err :: SomeException))
|
||||||
redirect
|
redirect
|
||||||
eres
|
eres
|
||||||
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
|
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
|
||||||
@ -67,17 +69,19 @@ authGoogleEmail =
|
|||||||
completeHelper posts
|
completeHelper posts
|
||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
completeHelper :: YesodAuth master => [(Text, Text)] -> AuthHandler master ()
|
completeHelper :: YesodAuth master => [(Text, Text)] -> AuthHandler master TypedContent
|
||||||
completeHelper gets' = do
|
completeHelper gets' = do
|
||||||
master <- lift getYesod
|
master <- lift getYesod
|
||||||
eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master)
|
eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master)
|
||||||
either onFailure onSuccess eres
|
tm <- getRouteToParent
|
||||||
|
either (onFailure tm) (onSuccess tm) eres
|
||||||
where
|
where
|
||||||
onFailure err = loginErrorMessage LoginR $ T.pack $ show (err :: SomeException)
|
onFailure tm err =
|
||||||
onSuccess oir = do
|
lift $ loginErrorMessage (tm LoginR) $ T.pack $ show (err :: SomeException)
|
||||||
|
onSuccess tm oir = do
|
||||||
let OpenId.Identifier ident = OpenId.oirOpLocal oir
|
let OpenId.Identifier ident = OpenId.oirOpLocal oir
|
||||||
memail <- lookupGetParam "openid.ext1.value.email"
|
memail <- lookupGetParam "openid.ext1.value.email"
|
||||||
case (memail, "https://www.google.com/accounts/o8/id" `T.isPrefixOf` ident) of
|
case (memail, "https://www.google.com/accounts/o8/id" `T.isPrefixOf` ident) of
|
||||||
(Just email, True) -> lift $ setCreds True $ Creds pid email []
|
(Just email, True) -> lift $ setCredsRedirect $ Creds pid email []
|
||||||
(_, False) -> loginErrorMessage LoginR "Only Google login is supported"
|
(_, False) -> lift $ loginErrorMessage (tm LoginR) "Only Google login is supported"
|
||||||
(Nothing, _) -> loginErrorMessage LoginR "No email address provided"
|
(Nothing, _) -> lift $ loginErrorMessage (tm LoginR) "No email address provided"
|
||||||
|
|||||||
202
yesod-auth/Yesod/Auth/GoogleEmail2.hs
Normal file
202
yesod-auth/Yesod/Auth/GoogleEmail2.hs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
-- | Use an email address as an identifier via Google's login system.
|
||||||
|
--
|
||||||
|
-- Note that this is a replacement for "Yesod.Auth.GoogleEmail", which depends
|
||||||
|
-- on Google's now deprecated OpenID system. For more information, see
|
||||||
|
-- <https://developers.google.com/+/api/auth-migration>.
|
||||||
|
--
|
||||||
|
-- By using this plugin, you are trusting Google to validate an email address,
|
||||||
|
-- and requiring users to have a Google account. On the plus side, you get to
|
||||||
|
-- use email addresses as the identifier, many users have existing Google
|
||||||
|
-- accounts, the login system has been long tested (as opposed to BrowserID),
|
||||||
|
-- and it requires no credential managing or setup (as opposed to Email).
|
||||||
|
--
|
||||||
|
-- In order to use this plugin:
|
||||||
|
--
|
||||||
|
-- * Create an application on the Google Developer Console <https://console.developers.google.com/>
|
||||||
|
--
|
||||||
|
-- * Create OAuth credentials. The redirect URI will be <http://yourdomain/auth/page/googleemail2/complete>. (If you have your authentication subsite at a different root than \/auth\/, please adjust accordingly.)
|
||||||
|
--
|
||||||
|
-- * Enable the Google+ API.
|
||||||
|
--
|
||||||
|
-- Since 1.3.1
|
||||||
|
module Yesod.Auth.GoogleEmail2
|
||||||
|
( authGoogleEmail
|
||||||
|
, forwardUrl
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blaze.ByteString.Builder (fromByteString, toByteString)
|
||||||
|
import Control.Applicative ((<$>), (<*>))
|
||||||
|
import Control.Arrow (second)
|
||||||
|
import Control.Monad (liftM, unless)
|
||||||
|
import Data.Aeson.Parser (json')
|
||||||
|
import Data.Aeson.Types (FromJSON (parseJSON), parseEither,
|
||||||
|
withObject)
|
||||||
|
import Data.Conduit (($$+-))
|
||||||
|
import Data.Conduit.Attoparsec (sinkParser)
|
||||||
|
import Data.Monoid (mappend)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||||
|
import Network.HTTP.Client (parseUrl, requestHeaders,
|
||||||
|
responseBody, urlEncodedBody)
|
||||||
|
import Network.HTTP.Conduit (http)
|
||||||
|
import Network.HTTP.Types (renderQueryText)
|
||||||
|
import Network.Mail.Mime (randomString)
|
||||||
|
import System.Random (newStdGen)
|
||||||
|
import Yesod.Auth (Auth, AuthPlugin (AuthPlugin),
|
||||||
|
AuthRoute, Creds (Creds),
|
||||||
|
Route (PluginR), YesodAuth,
|
||||||
|
authHttpManager, setCredsRedirect)
|
||||||
|
import qualified Yesod.Auth.Message as Msg
|
||||||
|
import Yesod.Core (HandlerSite, MonadHandler,
|
||||||
|
getRouteToParent, getUrlRender,
|
||||||
|
getYesod, invalidArgs, lift,
|
||||||
|
lookupGetParam,
|
||||||
|
lookupSession, notFound, redirect,
|
||||||
|
setSession, whamlet, (.:),
|
||||||
|
TypedContent, HandlerT, liftIO)
|
||||||
|
|
||||||
|
pid :: Text
|
||||||
|
pid = "googleemail2"
|
||||||
|
|
||||||
|
forwardUrl :: AuthRoute
|
||||||
|
forwardUrl = PluginR pid ["forward"]
|
||||||
|
|
||||||
|
csrfKey :: Text
|
||||||
|
csrfKey = "_GOOGLE_CSRF_TOKEN"
|
||||||
|
|
||||||
|
getCsrfToken :: MonadHandler m => m (Maybe Text)
|
||||||
|
getCsrfToken = lookupSession csrfKey
|
||||||
|
|
||||||
|
getCreateCsrfToken :: MonadHandler m => m Text
|
||||||
|
getCreateCsrfToken = do
|
||||||
|
mtoken <- getCsrfToken
|
||||||
|
case mtoken of
|
||||||
|
Just token -> return token
|
||||||
|
Nothing -> do
|
||||||
|
stdgen <- liftIO newStdGen
|
||||||
|
let token = T.pack $ fst $ randomString 10 stdgen
|
||||||
|
setSession csrfKey token
|
||||||
|
return token
|
||||||
|
|
||||||
|
authGoogleEmail :: YesodAuth m
|
||||||
|
=> Text -- ^ client ID
|
||||||
|
-> Text -- ^ client secret
|
||||||
|
-> AuthPlugin m
|
||||||
|
authGoogleEmail clientID clientSecret =
|
||||||
|
AuthPlugin pid dispatch login
|
||||||
|
where
|
||||||
|
complete = PluginR pid ["complete"]
|
||||||
|
|
||||||
|
getDest :: MonadHandler m
|
||||||
|
=> (Route Auth -> Route (HandlerSite m))
|
||||||
|
-> m Text
|
||||||
|
getDest tm = do
|
||||||
|
csrf <- getCreateCsrfToken
|
||||||
|
render <- getUrlRender
|
||||||
|
let qs = map (second Just)
|
||||||
|
[ ("scope", "email")
|
||||||
|
, ("state", csrf)
|
||||||
|
, ("redirect_uri", render $ tm complete)
|
||||||
|
, ("response_type", "code")
|
||||||
|
, ("client_id", clientID)
|
||||||
|
, ("access_type", "offline")
|
||||||
|
]
|
||||||
|
return $ decodeUtf8
|
||||||
|
$ toByteString
|
||||||
|
$ fromByteString "https://accounts.google.com/o/oauth2/auth"
|
||||||
|
`mappend` renderQueryText True qs
|
||||||
|
|
||||||
|
login tm = do
|
||||||
|
url <- getDest tm
|
||||||
|
[whamlet|<a href=#{url}>_{Msg.LoginGoogle}|]
|
||||||
|
|
||||||
|
dispatch :: YesodAuth site
|
||||||
|
=> Text
|
||||||
|
-> [Text]
|
||||||
|
-> HandlerT Auth (HandlerT site IO) TypedContent
|
||||||
|
dispatch "GET" ["forward"] = do
|
||||||
|
tm <- getRouteToParent
|
||||||
|
lift (getDest tm) >>= redirect
|
||||||
|
|
||||||
|
dispatch "GET" ["complete"] = do
|
||||||
|
mstate <- lookupGetParam "state"
|
||||||
|
case mstate of
|
||||||
|
Nothing -> invalidArgs ["CSRF state from Google is missing"]
|
||||||
|
Just state -> do
|
||||||
|
mtoken <- getCsrfToken
|
||||||
|
unless (Just state == mtoken) $ invalidArgs ["Invalid CSRF token from Google"]
|
||||||
|
mcode <- lookupGetParam "code"
|
||||||
|
code <-
|
||||||
|
case mcode of
|
||||||
|
Nothing -> invalidArgs ["Missing code paramter"]
|
||||||
|
Just c -> return c
|
||||||
|
|
||||||
|
render <- getUrlRender
|
||||||
|
|
||||||
|
req' <- liftIO $ parseUrl "https://accounts.google.com/o/oauth2/token" -- FIXME don't hardcode, use: https://accounts.google.com/.well-known/openid-configuration
|
||||||
|
let req =
|
||||||
|
urlEncodedBody
|
||||||
|
[ ("code", encodeUtf8 code)
|
||||||
|
, ("client_id", encodeUtf8 clientID)
|
||||||
|
, ("client_secret", encodeUtf8 clientSecret)
|
||||||
|
, ("redirect_uri", encodeUtf8 $ render complete)
|
||||||
|
, ("grant_type", "authorization_code")
|
||||||
|
]
|
||||||
|
req'
|
||||||
|
{ requestHeaders = []
|
||||||
|
}
|
||||||
|
manager <- liftM authHttpManager $ lift getYesod
|
||||||
|
res <- http req manager
|
||||||
|
value <- responseBody res $$+- sinkParser json'
|
||||||
|
Tokens accessToken _idToken tokenType <-
|
||||||
|
case parseEither parseJSON value of
|
||||||
|
Left e -> error e
|
||||||
|
Right t -> return t
|
||||||
|
|
||||||
|
unless (tokenType == "Bearer") $ error $ "Unknown token type: " ++ show tokenType
|
||||||
|
|
||||||
|
req2' <- liftIO $ parseUrl "https://www.googleapis.com/plus/v1/people/me"
|
||||||
|
let req2 = req2'
|
||||||
|
{ requestHeaders =
|
||||||
|
[ ("Authorization", encodeUtf8 $ "Bearer " `mappend` accessToken)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res2 <- http req2 manager
|
||||||
|
value2 <- responseBody res2 $$+- sinkParser json'
|
||||||
|
Person emails <-
|
||||||
|
case parseEither parseJSON value2 of
|
||||||
|
Left e -> error e
|
||||||
|
Right x -> return x
|
||||||
|
email <-
|
||||||
|
case map emailValue $ filter (\e -> emailType e == "account") emails of
|
||||||
|
[e] -> return e
|
||||||
|
[] -> error "No account email"
|
||||||
|
x -> error $ "Too many account emails: " ++ show x
|
||||||
|
lift $ setCredsRedirect $ Creds pid email []
|
||||||
|
|
||||||
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
|
data Tokens = Tokens Text Text Text
|
||||||
|
instance FromJSON Tokens where
|
||||||
|
parseJSON = withObject "Tokens" $ \o -> Tokens
|
||||||
|
<$> o .: "access_token"
|
||||||
|
<*> o .: "id_token"
|
||||||
|
<*> o .: "token_type"
|
||||||
|
|
||||||
|
data Person = Person [Email]
|
||||||
|
instance FromJSON Person where
|
||||||
|
parseJSON = withObject "Person" $ \o -> Person
|
||||||
|
<$> o .: "emails"
|
||||||
|
|
||||||
|
data Email = Email
|
||||||
|
{ emailValue :: Text
|
||||||
|
, emailType :: Text
|
||||||
|
}
|
||||||
|
deriving Show
|
||||||
|
instance FromJSON Email where
|
||||||
|
parseJSON = withObject "Email" $ \o -> Email
|
||||||
|
<$> o .: "value"
|
||||||
|
<*> o .: "type"
|
||||||
@ -1,268 +0,0 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE GADTs #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
-------------------------------------------------------------------------------
|
|
||||||
-- |
|
|
||||||
-- Module : Yesod.Auth.HashDB
|
|
||||||
-- Copyright : (c) Patrick Brisbin 2010
|
|
||||||
-- License : as-is
|
|
||||||
--
|
|
||||||
-- Maintainer : pbrisbin@gmail.com
|
|
||||||
-- Stability : Stable
|
|
||||||
-- Portability : Portable
|
|
||||||
--
|
|
||||||
-- A yesod-auth AuthPlugin designed to look users up in Persist where
|
|
||||||
-- their user id's and a salted SHA1 hash of their password is stored.
|
|
||||||
--
|
|
||||||
-- Example usage:
|
|
||||||
--
|
|
||||||
-- > -- import the function
|
|
||||||
-- > import Auth.HashDB
|
|
||||||
-- >
|
|
||||||
-- > -- make sure you have an auth route
|
|
||||||
-- > mkYesodData "MyApp" [$parseRoutes|
|
|
||||||
-- > / RootR GET
|
|
||||||
-- > /auth AuthR Auth getAuth
|
|
||||||
-- > |]
|
|
||||||
-- >
|
|
||||||
-- >
|
|
||||||
-- > -- make your app an instance of YesodAuth using this plugin
|
|
||||||
-- > instance YesodAuth MyApp where
|
|
||||||
-- > type AuthId MyApp = UserId
|
|
||||||
-- >
|
|
||||||
-- > loginDest _ = RootR
|
|
||||||
-- > logoutDest _ = RootR
|
|
||||||
-- > getAuthId = getAuthIdHashDB AuthR (Just . UniqueUser)
|
|
||||||
-- > authPlugins = [authHashDB (Just . UniqueUser)]
|
|
||||||
-- >
|
|
||||||
-- >
|
|
||||||
-- > -- include the migration function in site startup
|
|
||||||
-- > withServer :: (Application -> IO a) -> IO a
|
|
||||||
-- > withServer f = withConnectionPool $ \p -> do
|
|
||||||
-- > runSqlPool (runMigration migrateUsers) p
|
|
||||||
-- > let h = DevSite p
|
|
||||||
--
|
|
||||||
-- Note that function which converts username to unique identifier must be same.
|
|
||||||
--
|
|
||||||
-- Your app must be an instance of YesodPersist. and the username,
|
|
||||||
-- salt and hashed-passwords should be added to the database.
|
|
||||||
--
|
|
||||||
-- > echo -n 'MySaltMyPassword' | sha1sum
|
|
||||||
--
|
|
||||||
-- can be used to get the hash from the commandline.
|
|
||||||
--
|
|
||||||
-------------------------------------------------------------------------------
|
|
||||||
module Yesod.Auth.HashDB
|
|
||||||
( HashDBUser(..)
|
|
||||||
, Unique (..)
|
|
||||||
, setPassword
|
|
||||||
-- * Authentification
|
|
||||||
, validateUser
|
|
||||||
, authHashDB
|
|
||||||
, getAuthIdHashDB
|
|
||||||
-- * Predefined data type
|
|
||||||
, User
|
|
||||||
, UserGeneric (..)
|
|
||||||
, UserId
|
|
||||||
, EntityField (..)
|
|
||||||
, migrateUsers
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Yesod.Persist
|
|
||||||
import Yesod.Form
|
|
||||||
import Yesod.Auth
|
|
||||||
import Yesod.Core
|
|
||||||
import Text.Hamlet (hamlet)
|
|
||||||
|
|
||||||
import Control.Applicative ((<$>), (<*>))
|
|
||||||
import Control.Monad (replicateM,liftM)
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as BS (pack)
|
|
||||||
import Data.Digest.Pure.SHA (sha1, showDigest)
|
|
||||||
import Data.Text (Text, pack, unpack, append)
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import System.Random (randomRIO)
|
|
||||||
-- | Interface for data type which holds user info. It's just a
|
|
||||||
-- collection of getters and setters
|
|
||||||
class HashDBUser user where
|
|
||||||
-- | Retrieve password hash from user data
|
|
||||||
userPasswordHash :: user -> Maybe Text
|
|
||||||
-- | Retrieve salt for password
|
|
||||||
userPasswordSalt :: user -> Maybe Text
|
|
||||||
|
|
||||||
-- | Deprecated for the better named setSaltAndPasswordHash
|
|
||||||
setUserHashAndSalt :: Text -- ^ Salt
|
|
||||||
-> Text -- ^ Password hash
|
|
||||||
-> user -> user
|
|
||||||
setUserHashAndSalt = setSaltAndPasswordHash
|
|
||||||
|
|
||||||
-- | a callback for setPassword
|
|
||||||
setSaltAndPasswordHash :: Text -- ^ Salt
|
|
||||||
-> Text -- ^ Password hash
|
|
||||||
-> user -> user
|
|
||||||
setSaltAndPasswordHash = setUserHashAndSalt
|
|
||||||
|
|
||||||
-- | Generate random salt. Length of 8 is chosen arbitrarily
|
|
||||||
randomSalt :: MonadIO m => m Text
|
|
||||||
randomSalt = pack `liftM` liftIO (replicateM 8 (randomRIO ('0','z')))
|
|
||||||
|
|
||||||
-- | Calculate salted hash using SHA1.
|
|
||||||
saltedHash :: Text -- ^ Salt
|
|
||||||
-> Text -- ^ Password
|
|
||||||
-> Text
|
|
||||||
saltedHash salt =
|
|
||||||
pack . showDigest . sha1 . BS.pack . unpack . append salt
|
|
||||||
|
|
||||||
-- | Set password for user. This function should be used for setting
|
|
||||||
-- passwords. It generates random salt and calculates proper hashes.
|
|
||||||
setPassword :: (MonadIO m, HashDBUser user) => Text -> user -> m user
|
|
||||||
setPassword pwd u = do salt <- randomSalt
|
|
||||||
return $ setSaltAndPasswordHash salt (saltedHash salt pwd) u
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------
|
|
||||||
-- Authentification
|
|
||||||
----------------------------------------------------------------
|
|
||||||
|
|
||||||
-- | Given a user ID and password in plaintext, validate them against
|
|
||||||
-- the database values.
|
|
||||||
validateUser :: ( YesodPersist yesod
|
|
||||||
, b ~ YesodPersistBackend yesod
|
|
||||||
, PersistMonadBackend (b (HandlerT yesod IO)) ~ PersistEntityBackend user
|
|
||||||
, PersistUnique (b (HandlerT yesod IO))
|
|
||||||
, PersistEntity user
|
|
||||||
, HashDBUser user
|
|
||||||
) =>
|
|
||||||
Unique user -- ^ User unique identifier
|
|
||||||
-> Text -- ^ Password in plaint-text
|
|
||||||
-> HandlerT yesod IO Bool
|
|
||||||
validateUser userID passwd = do
|
|
||||||
-- Checks that hash and password match
|
|
||||||
let validate u = do hash <- userPasswordHash u
|
|
||||||
salt <- userPasswordSalt u
|
|
||||||
return $ hash == saltedHash salt passwd
|
|
||||||
-- Get user data
|
|
||||||
user <- runDB $ getBy userID
|
|
||||||
return $ fromMaybe False $ validate . entityVal =<< user
|
|
||||||
|
|
||||||
|
|
||||||
login :: AuthRoute
|
|
||||||
login = PluginR "hashdb" ["login"]
|
|
||||||
|
|
||||||
|
|
||||||
-- | Handle the login form. First parameter is function which maps
|
|
||||||
-- username (whatever it might be) to unique user ID.
|
|
||||||
postLoginR :: ( YesodAuth y, YesodPersist y
|
|
||||||
, HashDBUser user, PersistEntity user
|
|
||||||
, b ~ YesodPersistBackend y
|
|
||||||
, PersistMonadBackend (b (HandlerT y IO)) ~ PersistEntityBackend user
|
|
||||||
, PersistUnique (b (HandlerT y IO))
|
|
||||||
)
|
|
||||||
=> (Text -> Maybe (Unique user))
|
|
||||||
-> HandlerT Auth (HandlerT y IO) ()
|
|
||||||
postLoginR uniq = do
|
|
||||||
(mu,mp) <- lift $ runInputPost $ (,)
|
|
||||||
<$> iopt textField "username"
|
|
||||||
<*> iopt textField "password"
|
|
||||||
|
|
||||||
isValid <- lift $ fromMaybe (return False)
|
|
||||||
(validateUser <$> (uniq =<< mu) <*> mp)
|
|
||||||
if isValid
|
|
||||||
then lift $ setCreds True $ Creds "hashdb" (fromMaybe "" mu) []
|
|
||||||
else loginErrorMessage LoginR "Invalid username/password"
|
|
||||||
|
|
||||||
|
|
||||||
-- | A drop in for the getAuthId method of your YesodAuth instance which
|
|
||||||
-- can be used if authHashDB is the only plugin in use.
|
|
||||||
getAuthIdHashDB :: ( YesodAuth master, YesodPersist master
|
|
||||||
, HashDBUser user, PersistEntity user
|
|
||||||
, Key user ~ AuthId master
|
|
||||||
, b ~ YesodPersistBackend master
|
|
||||||
, PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend user
|
|
||||||
, PersistUnique (b (HandlerT master IO))
|
|
||||||
)
|
|
||||||
=> (AuthRoute -> Route master) -- ^ your site's Auth Route
|
|
||||||
-> (Text -> Maybe (Unique user)) -- ^ gets user ID
|
|
||||||
-> Creds master -- ^ the creds argument
|
|
||||||
-> HandlerT master IO (Maybe (AuthId master))
|
|
||||||
getAuthIdHashDB authR uniq creds = do
|
|
||||||
muid <- maybeAuthId
|
|
||||||
case muid of
|
|
||||||
-- user already authenticated
|
|
||||||
Just uid -> return $ Just uid
|
|
||||||
Nothing -> do
|
|
||||||
x <- case uniq (credsIdent creds) of
|
|
||||||
Nothing -> return Nothing
|
|
||||||
Just u -> runDB (getBy u)
|
|
||||||
case x of
|
|
||||||
-- user exists
|
|
||||||
Just (Entity uid _) -> return $ Just uid
|
|
||||||
Nothing -> loginErrorMessage (authR LoginR) "User not found"
|
|
||||||
|
|
||||||
-- | Prompt for username and password, validate that against a database
|
|
||||||
-- which holds the username and a hash of the password
|
|
||||||
authHashDB :: ( YesodAuth m, YesodPersist m
|
|
||||||
, HashDBUser user
|
|
||||||
, PersistEntity user
|
|
||||||
, b ~ YesodPersistBackend m
|
|
||||||
, PersistMonadBackend (b (HandlerT m IO)) ~ PersistEntityBackend user
|
|
||||||
, PersistUnique (b (HandlerT m IO)))
|
|
||||||
=> (Text -> Maybe (Unique user)) -> AuthPlugin m
|
|
||||||
authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> toWidget [hamlet|
|
|
||||||
$newline never
|
|
||||||
<div id="header">
|
|
||||||
<h1>Login
|
|
||||||
|
|
||||||
<div id="login">
|
|
||||||
<form method="post" action="@{tm login}">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Username:
|
|
||||||
<td>
|
|
||||||
<input id="x" name="username" autofocus="" required>
|
|
||||||
<tr>
|
|
||||||
<th>Password:
|
|
||||||
<td>
|
|
||||||
<input type="password" name="password" required>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<td>
|
|
||||||
<input type="submit" value="Login">
|
|
||||||
|
|
||||||
<script>
|
|
||||||
if (!("autofocus" in document.createElement("input"))) {
|
|
||||||
document.getElementById("x").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
|]
|
|
||||||
where
|
|
||||||
dispatch "POST" ["login"] = postLoginR uniq >>= sendResponse
|
|
||||||
dispatch _ _ = notFound
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------
|
|
||||||
-- Predefined datatype
|
|
||||||
----------------------------------------------------------------
|
|
||||||
|
|
||||||
-- | Generate data base instances for a valid user
|
|
||||||
share [mkPersist sqlSettings, mkMigrate "migrateUsers"]
|
|
||||||
[persistUpperCase|
|
|
||||||
User
|
|
||||||
username Text Eq
|
|
||||||
password Text
|
|
||||||
salt Text
|
|
||||||
UniqueUser username
|
|
||||||
|]
|
|
||||||
|
|
||||||
instance HashDBUser (UserGeneric backend) where
|
|
||||||
userPasswordHash = Just . userPassword
|
|
||||||
userPasswordSalt = Just . userSalt
|
|
||||||
setSaltAndPasswordHash s h u = u { userSalt = s
|
|
||||||
, userPassword = h
|
|
||||||
}
|
|
||||||
@ -14,6 +14,7 @@ module Yesod.Auth.Message
|
|||||||
, finnishMessage
|
, finnishMessage
|
||||||
, chineseMessage
|
, chineseMessage
|
||||||
, spanishMessage
|
, spanishMessage
|
||||||
|
, czechMessage
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Monoid (mappend)
|
import Data.Monoid (mappend)
|
||||||
@ -25,6 +26,7 @@ data AuthMessage =
|
|||||||
| LoginGoogle
|
| LoginGoogle
|
||||||
| LoginYahoo
|
| LoginYahoo
|
||||||
| Email
|
| Email
|
||||||
|
| IdentifierNotFound Text
|
||||||
| Password
|
| Password
|
||||||
| Register
|
| Register
|
||||||
| RegisterLong
|
| RegisterLong
|
||||||
@ -101,6 +103,7 @@ englishMessage ProvideIdentifier = "Email or Username"
|
|||||||
englishMessage SendPasswordResetEmail = "Send password reset email"
|
englishMessage SendPasswordResetEmail = "Send password reset email"
|
||||||
englishMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
englishMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
||||||
englishMessage InvalidUsernamePass = "Invalid username/password combination"
|
englishMessage InvalidUsernamePass = "Invalid username/password combination"
|
||||||
|
englishMessage (IdentifierNotFound ident) = "Login not found: " `mappend` ident
|
||||||
|
|
||||||
portugueseMessage :: AuthMessage -> Text
|
portugueseMessage :: AuthMessage -> Text
|
||||||
portugueseMessage NoOpenID = "Nenhum identificador OpenID encontrado"
|
portugueseMessage NoOpenID = "Nenhum identificador OpenID encontrado"
|
||||||
@ -142,6 +145,8 @@ portugueseMessage ProvideIdentifier = "E-mail ou nome de usuário"
|
|||||||
portugueseMessage SendPasswordResetEmail = "Enviar e-mail para resetar senha"
|
portugueseMessage SendPasswordResetEmail = "Enviar e-mail para resetar senha"
|
||||||
portugueseMessage PasswordResetPrompt = "Insira seu endereço de e-mail ou nome de usuário abaixo. Um e-mail para resetar sua senha será enviado para você."
|
portugueseMessage PasswordResetPrompt = "Insira seu endereço de e-mail ou nome de usuário abaixo. Um e-mail para resetar sua senha será enviado para você."
|
||||||
portugueseMessage InvalidUsernamePass = "Nome de usuário ou senha inválidos"
|
portugueseMessage InvalidUsernamePass = "Nome de usuário ou senha inválidos"
|
||||||
|
-- TODO
|
||||||
|
portugueseMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
spanishMessage :: AuthMessage -> Text
|
spanishMessage :: AuthMessage -> Text
|
||||||
spanishMessage NoOpenID = "No se encuentra el identificador OpenID"
|
spanishMessage NoOpenID = "No se encuentra el identificador OpenID"
|
||||||
@ -183,6 +188,8 @@ spanishMessage ProvideIdentifier = "Cuenta de correo o nombre de usuario"
|
|||||||
spanishMessage SendPasswordResetEmail = "Correo de actualización de contraseña enviado"
|
spanishMessage SendPasswordResetEmail = "Correo de actualización de contraseña enviado"
|
||||||
spanishMessage PasswordResetPrompt = "Escriba su cuenta de correo o nombre de usuario, y una confirmación de actualización de contraseña será enviada a su cuenta de correo."
|
spanishMessage PasswordResetPrompt = "Escriba su cuenta de correo o nombre de usuario, y una confirmación de actualización de contraseña será enviada a su cuenta de correo."
|
||||||
spanishMessage InvalidUsernamePass = "Combinación de nombre de usuario/contraseña invalida"
|
spanishMessage InvalidUsernamePass = "Combinación de nombre de usuario/contraseña invalida"
|
||||||
|
-- TODO
|
||||||
|
spanishMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
swedishMessage :: AuthMessage -> Text
|
swedishMessage :: AuthMessage -> Text
|
||||||
swedishMessage NoOpenID = "Fann ej OpenID identifierare"
|
swedishMessage NoOpenID = "Fann ej OpenID identifierare"
|
||||||
@ -225,6 +232,8 @@ swedishMessage SendPasswordResetEmail = "Skicka email för återställning av l
|
|||||||
swedishMessage PasswordResetPrompt = "Skriv in din emailadress eller användarnamn nedan och " `mappend`
|
swedishMessage PasswordResetPrompt = "Skriv in din emailadress eller användarnamn nedan och " `mappend`
|
||||||
"ett email för återställning av lösenord kommmer att skickas till dig."
|
"ett email för återställning av lösenord kommmer att skickas till dig."
|
||||||
swedishMessage InvalidUsernamePass = "Ogiltig kombination av användarnamn och lösenord"
|
swedishMessage InvalidUsernamePass = "Ogiltig kombination av användarnamn och lösenord"
|
||||||
|
-- TODO
|
||||||
|
swedishMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
germanMessage :: AuthMessage -> Text
|
germanMessage :: AuthMessage -> Text
|
||||||
germanMessage NoOpenID = "Kein OpenID-Identifier gefunden"
|
germanMessage NoOpenID = "Kein OpenID-Identifier gefunden"
|
||||||
@ -266,6 +275,8 @@ germanMessage ProvideIdentifier = "Email-Adresse oder Nutzername"
|
|||||||
germanMessage SendPasswordResetEmail = "Email zusenden um Passwort zurückzusetzen"
|
germanMessage SendPasswordResetEmail = "Email zusenden um Passwort zurückzusetzen"
|
||||||
germanMessage PasswordResetPrompt = "Nach Einhabe der Email-Adresse oder des Nutzernamen wird eine Email zugesendet mit welcher das Passwort zurückgesetzt werden kann."
|
germanMessage PasswordResetPrompt = "Nach Einhabe der Email-Adresse oder des Nutzernamen wird eine Email zugesendet mit welcher das Passwort zurückgesetzt werden kann."
|
||||||
germanMessage InvalidUsernamePass = "Ungültige Kombination aus Nutzername und Passwort"
|
germanMessage InvalidUsernamePass = "Ungültige Kombination aus Nutzername und Passwort"
|
||||||
|
-- TODO
|
||||||
|
germanMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
frenchMessage :: AuthMessage -> Text
|
frenchMessage :: AuthMessage -> Text
|
||||||
frenchMessage NoOpenID = "Aucun fournisseur OpenID n'a été trouvé"
|
frenchMessage NoOpenID = "Aucun fournisseur OpenID n'a été trouvé"
|
||||||
@ -300,13 +311,14 @@ frenchMessage NowLoggedIn = "Vous êtes maintenant connecté"
|
|||||||
frenchMessage LoginTitle = "Se connecter"
|
frenchMessage LoginTitle = "Se connecter"
|
||||||
frenchMessage PleaseProvideUsername = "Merci de renseigner votre nom d'utilisateur"
|
frenchMessage PleaseProvideUsername = "Merci de renseigner votre nom d'utilisateur"
|
||||||
frenchMessage PleaseProvidePassword = "Merci de spécifier un mot de passe"
|
frenchMessage PleaseProvidePassword = "Merci de spécifier un mot de passe"
|
||||||
frenchMessage NoIdentifierProvided = "No email/username provided"
|
frenchMessage NoIdentifierProvided = "Adresse électronique/nom d'utilisateur non spécifié"
|
||||||
frenchMessage InvalidEmailAddress = "Invalid email address provided"
|
frenchMessage InvalidEmailAddress = "Adresse électronique spécifiée invalide"
|
||||||
frenchMessage PasswordResetTitle = "Password Reset"
|
frenchMessage PasswordResetTitle = "Réinitialisation de mot de passe"
|
||||||
frenchMessage ProvideIdentifier = "Email or Username"
|
frenchMessage ProvideIdentifier = "Adresse électronique ou nom d'utilisateur"
|
||||||
frenchMessage SendPasswordResetEmail = "Send password reset email"
|
frenchMessage SendPasswordResetEmail = "Envoie d'un message électronique pour Réinitialisation le mot de passe"
|
||||||
frenchMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
frenchMessage PasswordResetPrompt = "Entrez votre adresse électronique ou votre nom d'utilisateur ci-dessous, et un message électronique de réinitialisation de mot de passe vous sera envoyé."
|
||||||
frenchMessage InvalidUsernamePass = "Invalid username/password combination"
|
frenchMessage InvalidUsernamePass = "Le couble nom d'utilisateur/mot de passe invalide"
|
||||||
|
frenchMessage (IdentifierNotFound ident) = "Nom d'utilisateur introuvable: " `mappend` ident
|
||||||
|
|
||||||
norwegianBokmålMessage :: AuthMessage -> Text
|
norwegianBokmålMessage :: AuthMessage -> Text
|
||||||
norwegianBokmålMessage NoOpenID = "Ingen OpenID-identifiserer funnet"
|
norwegianBokmålMessage NoOpenID = "Ingen OpenID-identifiserer funnet"
|
||||||
@ -348,6 +360,8 @@ norwegianBokmålMessage ProvideIdentifier = "Email or Username"
|
|||||||
norwegianBokmålMessage SendPasswordResetEmail = "Send password reset email"
|
norwegianBokmålMessage SendPasswordResetEmail = "Send password reset email"
|
||||||
norwegianBokmålMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
norwegianBokmålMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
||||||
norwegianBokmålMessage InvalidUsernamePass = "Invalid username/password combination"
|
norwegianBokmålMessage InvalidUsernamePass = "Invalid username/password combination"
|
||||||
|
-- TODO
|
||||||
|
norwegianBokmålMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
japaneseMessage :: AuthMessage -> Text
|
japaneseMessage :: AuthMessage -> Text
|
||||||
japaneseMessage NoOpenID = "OpenID識別子がありません"
|
japaneseMessage NoOpenID = "OpenID識別子がありません"
|
||||||
@ -389,6 +403,8 @@ japaneseMessage ProvideIdentifier = "Email or Username"
|
|||||||
japaneseMessage SendPasswordResetEmail = "Send password reset email"
|
japaneseMessage SendPasswordResetEmail = "Send password reset email"
|
||||||
japaneseMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
japaneseMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you."
|
||||||
japaneseMessage InvalidUsernamePass = "Invalid username/password combination"
|
japaneseMessage InvalidUsernamePass = "Invalid username/password combination"
|
||||||
|
japaneseMessage (IdentifierNotFound ident) =
|
||||||
|
"「" `mappend` ident `mappend` "」は正しくないログインので、または未入力の項目があります。"
|
||||||
|
|
||||||
finnishMessage :: AuthMessage -> Text
|
finnishMessage :: AuthMessage -> Text
|
||||||
finnishMessage NoOpenID = "OpenID-tunnistetta ei löydy"
|
finnishMessage NoOpenID = "OpenID-tunnistetta ei löydy"
|
||||||
@ -431,6 +447,8 @@ finnishMessage ProvideIdentifier = "Sähköpostiosoite tai käyttäjänimi"
|
|||||||
finnishMessage SendPasswordResetEmail = "Lähetä uusi salasana sähköpostitse"
|
finnishMessage SendPasswordResetEmail = "Lähetä uusi salasana sähköpostitse"
|
||||||
finnishMessage PasswordResetPrompt = "Anna sähköpostiosoitteesi tai käyttäjätunnuksesi alla, niin lähetämme uuden salasanan sähköpostitse."
|
finnishMessage PasswordResetPrompt = "Anna sähköpostiosoitteesi tai käyttäjätunnuksesi alla, niin lähetämme uuden salasanan sähköpostitse."
|
||||||
finnishMessage InvalidUsernamePass = "Virheellinen käyttäjänimi tai salasana."
|
finnishMessage InvalidUsernamePass = "Virheellinen käyttäjänimi tai salasana."
|
||||||
|
-- TODO
|
||||||
|
finnishMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
chineseMessage :: AuthMessage -> Text
|
chineseMessage :: AuthMessage -> Text
|
||||||
chineseMessage NoOpenID = "无效的OpenID"
|
chineseMessage NoOpenID = "无效的OpenID"
|
||||||
@ -472,5 +490,46 @@ chineseMessage ProvideIdentifier = "邮箱或用户名"
|
|||||||
chineseMessage SendPasswordResetEmail = "发送密码重置邮件"
|
chineseMessage SendPasswordResetEmail = "发送密码重置邮件"
|
||||||
chineseMessage PasswordResetPrompt = "输入你的邮箱地址或用户名,你将收到一封密码重置邮件。"
|
chineseMessage PasswordResetPrompt = "输入你的邮箱地址或用户名,你将收到一封密码重置邮件。"
|
||||||
chineseMessage InvalidUsernamePass = "无效的用户名/密码组合"
|
chineseMessage InvalidUsernamePass = "无效的用户名/密码组合"
|
||||||
|
-- TODO
|
||||||
|
chineseMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|
||||||
|
czechMessage :: AuthMessage -> Text
|
||||||
|
czechMessage NoOpenID = "Nebyl nalezen identifikátor OpenID"
|
||||||
|
czechMessage LoginOpenID = "Přihlásit přes OpenID"
|
||||||
|
czechMessage LoginGoogle = "Přihlásit přes Google"
|
||||||
|
czechMessage LoginYahoo = "Přihlásit přes Yahoo"
|
||||||
|
czechMessage Email = "E-mail"
|
||||||
|
czechMessage Password = "Heslo"
|
||||||
|
czechMessage Register = "Registrovat"
|
||||||
|
czechMessage RegisterLong = "Zaregistrovat nový účet"
|
||||||
|
czechMessage EnterEmail = "Níže zadejte svou e-mailovou adresu a bude vám poslán potvrzovací e-mail."
|
||||||
|
czechMessage ConfirmationEmailSentTitle = "Potvrzovací e-mail odeslán"
|
||||||
|
czechMessage (ConfirmationEmailSent email) =
|
||||||
|
"Potvrzovací e-mail byl odeslán na " `mappend` email `mappend` "."
|
||||||
|
czechMessage AddressVerified = "Adresa byla ověřena, prosím nastavte si nové heslo"
|
||||||
|
czechMessage InvalidKeyTitle = "Neplatný ověřovací klíč"
|
||||||
|
czechMessage InvalidKey = "Bohužel, ověřovací klíč je neplatný."
|
||||||
|
czechMessage InvalidEmailPass = "Neplatná kombinace e-mail/heslo"
|
||||||
|
czechMessage BadSetPass = "Pro nastavení hesla je vyžadováno přihlášení"
|
||||||
|
czechMessage SetPassTitle = "Nastavit heslo"
|
||||||
|
czechMessage SetPass = "Nastavit nové heslo"
|
||||||
|
czechMessage NewPass = "Nové heslo"
|
||||||
|
czechMessage ConfirmPass = "Potvrdit"
|
||||||
|
czechMessage PassMismatch = "Hesla si neodpovídají, zkuste to znovu"
|
||||||
|
czechMessage PassUpdated = "Heslo aktualizováno"
|
||||||
|
czechMessage Facebook = "Přihlásit přes Facebook"
|
||||||
|
czechMessage LoginViaEmail = "Přihlásit přes e-mail"
|
||||||
|
czechMessage InvalidLogin = "Neplatné přihlášení"
|
||||||
|
czechMessage NowLoggedIn = "Přihlášení proběhlo úspěšně"
|
||||||
|
czechMessage LoginTitle = "Přihlásit"
|
||||||
|
czechMessage PleaseProvideUsername = "Prosím, zadejte svoje uživatelské jméno"
|
||||||
|
czechMessage PleaseProvidePassword = "Prosím, zadejte svoje heslo"
|
||||||
|
czechMessage NoIdentifierProvided = "Nebyl poskytnut žádný e-mail nebo uživatelské jméno"
|
||||||
|
czechMessage InvalidEmailAddress = "Zadaná e-mailová adresa je neplatná"
|
||||||
|
czechMessage PasswordResetTitle = "Obnovení hesla"
|
||||||
|
czechMessage ProvideIdentifier = "E-mail nebo uživatelské jméno"
|
||||||
|
czechMessage SendPasswordResetEmail = "Poslat e-mail pro obnovení hesla"
|
||||||
|
czechMessage PasswordResetPrompt = "Zadejte svou e-mailovou adresu nebo uživatelské jméno a bude vám poslán email pro obnovení hesla."
|
||||||
|
czechMessage InvalidUsernamePass = "Neplatná kombinace uživatelského jména a hesla"
|
||||||
|
-- TODO
|
||||||
|
czechMessage i@(IdentifierNotFound _) = englishMessage i
|
||||||
|
|||||||
@ -69,8 +69,10 @@ $newline never
|
|||||||
master <- lift getYesod
|
master <- lift getYesod
|
||||||
eres <- lift $ try $ OpenId.getForwardUrl oid complete' Nothing extensionFields (authHttpManager master)
|
eres <- lift $ try $ OpenId.getForwardUrl oid complete' Nothing extensionFields (authHttpManager master)
|
||||||
case eres of
|
case eres of
|
||||||
Left err -> loginErrorMessage LoginR $ T.pack $
|
Left err -> do
|
||||||
show (err :: SomeException)
|
tm <- getRouteToParent
|
||||||
|
lift $ loginErrorMessage (tm LoginR) $ T.pack $
|
||||||
|
show (err :: SomeException)
|
||||||
Right x -> redirect x
|
Right x -> redirect x
|
||||||
Nothing -> loginErrorMessageI LoginR Msg.NoOpenID
|
Nothing -> loginErrorMessageI LoginR Msg.NoOpenID
|
||||||
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
|
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
|
||||||
@ -83,14 +85,16 @@ $newline never
|
|||||||
completeHelper idType posts
|
completeHelper idType posts
|
||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
completeHelper :: IdentifierType -> [(Text, Text)] -> AuthHandler master ()
|
completeHelper :: IdentifierType -> [(Text, Text)] -> AuthHandler master TypedContent
|
||||||
completeHelper idType gets' = do
|
completeHelper idType gets' = do
|
||||||
master <- lift getYesod
|
master <- lift getYesod
|
||||||
eres <- try $ OpenId.authenticateClaimed gets' (authHttpManager master)
|
eres <- try $ OpenId.authenticateClaimed gets' (authHttpManager master)
|
||||||
either onFailure onSuccess eres
|
either onFailure onSuccess eres
|
||||||
where
|
where
|
||||||
onFailure err = loginErrorMessage LoginR $ T.pack $
|
onFailure err = do
|
||||||
show (err :: SomeException)
|
tm <- getRouteToParent
|
||||||
|
lift $ loginErrorMessage (tm LoginR) $ T.pack $
|
||||||
|
show (err :: SomeException)
|
||||||
onSuccess oir = do
|
onSuccess oir = do
|
||||||
let claimed =
|
let claimed =
|
||||||
case OpenId.oirClaimed oir of
|
case OpenId.oirClaimed oir of
|
||||||
@ -104,7 +108,7 @@ completeHelper idType gets' = do
|
|||||||
case idType of
|
case idType of
|
||||||
OPLocal -> OpenId.oirOpLocal oir
|
OPLocal -> OpenId.oirOpLocal oir
|
||||||
Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir
|
Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir
|
||||||
lift $ setCreds True $ Creds "openid" i gets''
|
lift $ setCredsRedirect $ Creds "openid" i gets''
|
||||||
|
|
||||||
-- | The main identifier provided by the OpenID authentication plugin is the
|
-- | The main identifier provided by the OpenID authentication plugin is the
|
||||||
-- \"OP-local identifier\". There is also sometimes a \"claimed\" identifier
|
-- \"OP-local identifier\". There is also sometimes a \"claimed\" identifier
|
||||||
|
|||||||
@ -48,7 +48,7 @@ $newline never
|
|||||||
$ maybe id (\x -> (:) ("displayName", x))
|
$ maybe id (\x -> (:) ("displayName", x))
|
||||||
(fmap pack $ getDisplayName $ map (unpack *** unpack) extra)
|
(fmap pack $ getDisplayName $ map (unpack *** unpack) extra)
|
||||||
[]
|
[]
|
||||||
lift $ setCreds True creds
|
lift $ setCredsRedirect creds
|
||||||
dispatch _ _ = notFound
|
dispatch _ _ = notFound
|
||||||
|
|
||||||
-- | Get some form of a display name.
|
-- | Get some form of a display name.
|
||||||
|
|||||||
429
yesod-auth/Yesod/PasswordStore.hs
Executable file
429
yesod-auth/Yesod/PasswordStore.hs
Executable file
@ -0,0 +1,429 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings, BangPatterns #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
-- |
|
||||||
|
-- Module : Crypto.PasswordStore
|
||||||
|
-- Copyright : (c) Peter Scott, 2011
|
||||||
|
-- License : BSD-style
|
||||||
|
--
|
||||||
|
-- Maintainer : pjscott@iastate.edu
|
||||||
|
-- Stability : experimental
|
||||||
|
-- Portability : portable
|
||||||
|
--
|
||||||
|
-- Securely store hashed, salted passwords. If you need to store and verify
|
||||||
|
-- passwords, there are many wrong ways to do it, most of them all too
|
||||||
|
-- common. Some people store users' passwords in plain text. Then, when an
|
||||||
|
-- attacker manages to get their hands on this file, they have the passwords for
|
||||||
|
-- every user's account. One step up, but still wrong, is to simply hash all
|
||||||
|
-- passwords with SHA1 or something. This is vulnerable to rainbow table and
|
||||||
|
-- dictionary attacks. One step up from that is to hash the password along with
|
||||||
|
-- a unique salt value. This is vulnerable to dictionary attacks, since guessing
|
||||||
|
-- a password is very fast. The right thing to do is to use a slow hash
|
||||||
|
-- function, to add some small but significant delay, that will be negligible
|
||||||
|
-- for legitimate users but prohibitively expensive for someone trying to guess
|
||||||
|
-- passwords by brute force. That is what this library does. It iterates a
|
||||||
|
-- SHA256 hash, with a random salt, a few thousand times. This scheme is known
|
||||||
|
-- as PBKDF1, and is generally considered secure; there is nothing innovative
|
||||||
|
-- happening here.
|
||||||
|
--
|
||||||
|
-- The API here is very simple. What you store are called /password hashes/.
|
||||||
|
-- They are strings (technically, ByteStrings) that look like this:
|
||||||
|
--
|
||||||
|
-- > "sha256|14|jEWU94phx4QzNyH94Qp4CQ==|5GEw+jxP/4WLgzt9VS3Ee3nhqBlDsrKiB+rq7JfMckU="
|
||||||
|
--
|
||||||
|
-- Each password hash shows the algorithm, the strength (more on that later),
|
||||||
|
-- the salt, and the hashed-and-salted password. You store these on your server,
|
||||||
|
-- in a database, for when you need to verify a password. You make a password
|
||||||
|
-- hash with the 'makePassword' function. Here's an example:
|
||||||
|
--
|
||||||
|
-- > >>> makePassword "hunter2" 14
|
||||||
|
-- > "sha256|14|Zo4LdZGrv/HYNAUG3q8WcA==|zKjbHZoTpuPLp1lh6ATolWGIKjhXvY4TysuKvqtNFyk="
|
||||||
|
--
|
||||||
|
-- This will hash the password @\"hunter2\"@, with strength 12, which is a good
|
||||||
|
-- default value. The strength here determines how long the hashing will
|
||||||
|
-- take. When doing the hashing, we iterate the SHA256 hash function
|
||||||
|
-- @2^strength@ times, so increasing the strength by 1 makes the hashing take
|
||||||
|
-- twice as long. When computers get faster, you can bump up the strength a
|
||||||
|
-- little bit to compensate. You can strengthen existing password hashes with
|
||||||
|
-- the 'strengthenPassword' function. Note that 'makePassword' needs to generate
|
||||||
|
-- random numbers, so its return type is 'IO' 'ByteString'. If you want to avoid
|
||||||
|
-- the 'IO' monad, you can generate your own salt and pass it to
|
||||||
|
-- 'makePasswordSalt'.
|
||||||
|
--
|
||||||
|
-- Your strength value should not be less than 12, and 14 is a good default
|
||||||
|
-- value at the time of this writing, in 2013.
|
||||||
|
--
|
||||||
|
-- Once you've got your password hashes, the second big thing you need to do
|
||||||
|
-- with them is verify passwords against them. When a user gives you a password,
|
||||||
|
-- you compare it with a password hash using the 'verifyPassword' function:
|
||||||
|
--
|
||||||
|
-- > >>> verifyPassword "wrong guess" passwordHash
|
||||||
|
-- > False
|
||||||
|
-- > >>> verifyPassword "hunter2" passwordHash
|
||||||
|
-- > True
|
||||||
|
--
|
||||||
|
-- These two functions are really all you need. If you want to make existing
|
||||||
|
-- password hashes stronger, you can use 'strengthenPassword'. Just pass it an
|
||||||
|
-- existing password hash and a new strength value, and it will return a new
|
||||||
|
-- password hash with that strength value, which will match the same password as
|
||||||
|
-- the old password hash.
|
||||||
|
--
|
||||||
|
-- Note that, as of version 2.4, you can also use PBKDF2, and specify the exact
|
||||||
|
-- iteration count. This does not have a significant effect on security, but can
|
||||||
|
-- be handy for compatibility with other code.
|
||||||
|
|
||||||
|
module Yesod.PasswordStore (
|
||||||
|
|
||||||
|
-- * Algorithms
|
||||||
|
pbkdf1, -- :: ByteString -> Salt -> Int -> ByteString
|
||||||
|
pbkdf2, -- :: ByteString -> Salt -> Int -> ByteString
|
||||||
|
|
||||||
|
-- * Registering and verifying passwords
|
||||||
|
makePassword, -- :: ByteString -> Int -> IO ByteString
|
||||||
|
makePasswordWith, -- :: (ByteString -> Salt -> Int -> ByteString) ->
|
||||||
|
-- ByteString -> Int -> IO ByteString
|
||||||
|
makePasswordSalt, -- :: ByteString -> ByteString -> Int -> ByteString
|
||||||
|
makePasswordSaltWith, -- :: (ByteString -> Salt -> Int -> ByteString) ->
|
||||||
|
-- ByteString -> Salt -> Int -> ByteString
|
||||||
|
verifyPassword, -- :: ByteString -> ByteString -> Bool
|
||||||
|
verifyPasswordWith, -- :: (ByteString -> Salt -> Int -> ByteString) ->
|
||||||
|
-- (Int -> Int) -> ByteString -> ByteString -> Bool
|
||||||
|
|
||||||
|
-- * Updating password hash strength
|
||||||
|
strengthenPassword, -- :: ByteString -> Int -> ByteString
|
||||||
|
passwordStrength, -- :: ByteString -> Int
|
||||||
|
|
||||||
|
-- * Utilities
|
||||||
|
Salt,
|
||||||
|
isPasswordFormatValid, -- :: ByteString -> Bool
|
||||||
|
genSaltIO, -- :: IO Salt
|
||||||
|
genSaltRandom, -- :: (RandomGen b) => b -> (Salt, b)
|
||||||
|
makeSalt, -- :: ByteString -> Salt
|
||||||
|
exportSalt, -- :: Salt -> ByteString
|
||||||
|
importSalt -- :: ByteString -> Salt
|
||||||
|
) where
|
||||||
|
|
||||||
|
|
||||||
|
import qualified Crypto.Hash as CH
|
||||||
|
import qualified Crypto.Hash.SHA256 as H
|
||||||
|
import qualified Data.ByteString.Char8 as B
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.ST
|
||||||
|
import Data.Byteable (toBytes)
|
||||||
|
import Data.STRef
|
||||||
|
import Data.Bits
|
||||||
|
import Data.ByteString.Char8 (ByteString)
|
||||||
|
import Data.ByteString.Base64 (encode, decodeLenient)
|
||||||
|
import System.IO
|
||||||
|
import System.Random
|
||||||
|
import Data.Maybe
|
||||||
|
import qualified Control.Exception
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- Cryptographic base
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- | PBKDF1 key-derivation function. Takes a password, a 'Salt', and a number of
|
||||||
|
-- iterations. The number of iterations should be at least 1000, and probably
|
||||||
|
-- more. 5000 is a reasonable number, computing almost instantaneously. This
|
||||||
|
-- will give a 32-byte 'ByteString' as output. Both the salt and this 32-byte
|
||||||
|
-- key should be stored in the password file. When a user wishes to authenticate
|
||||||
|
-- a password, just pass it and the salt to this function, and see if the output
|
||||||
|
-- matches.
|
||||||
|
pbkdf1 :: ByteString -> Salt -> Int -> ByteString
|
||||||
|
pbkdf1 password (SaltBS salt) iter = hashRounds first_hash (iter + 1)
|
||||||
|
where first_hash = H.finalize $ H.init `H.update` password `H.update` salt
|
||||||
|
|
||||||
|
-- | Hash a 'ByteString' for a given number of rounds. The number of rounds is 0
|
||||||
|
-- or more. If the number of rounds specified is 0, the ByteString will be
|
||||||
|
-- returned unmodified.
|
||||||
|
hashRounds :: ByteString -> Int -> ByteString
|
||||||
|
hashRounds (!bs) 0 = bs
|
||||||
|
hashRounds bs rounds = hashRounds (H.hash bs) (rounds - 1)
|
||||||
|
|
||||||
|
-- | Computes the hmacSHA256 of the given message, with the given 'Salt'.
|
||||||
|
hmacSHA256 :: ByteString
|
||||||
|
-- ^ The secret (the salt)
|
||||||
|
-> ByteString
|
||||||
|
-- ^ The clear-text message
|
||||||
|
-> ByteString
|
||||||
|
-- ^ The encoded message
|
||||||
|
hmacSHA256 secret msg =
|
||||||
|
toBytes (CH.hmacGetDigest (CH.hmac secret msg) :: CH.Digest CH.SHA256)
|
||||||
|
|
||||||
|
-- | PBKDF2 key-derivation function.
|
||||||
|
-- For details see @http://tools.ietf.org/html/rfc2898@.
|
||||||
|
-- @32@ is the most common digest size for @SHA256@, and is
|
||||||
|
-- what the algorithm internally uses.
|
||||||
|
-- @HMAC+SHA256@ is used as @PRF@, because @HMAC+SHA1@ is considered too weak.
|
||||||
|
pbkdf2 :: ByteString -> Salt -> Int -> ByteString
|
||||||
|
pbkdf2 password (SaltBS salt) c =
|
||||||
|
let hLen = 32
|
||||||
|
dkLen = hLen in go hLen dkLen
|
||||||
|
where
|
||||||
|
go hLen dkLen | dkLen > (2^32 - 1) * hLen = error "Derived key too long."
|
||||||
|
| otherwise =
|
||||||
|
let !l = ceiling ((fromIntegral dkLen / fromIntegral hLen) :: Double)
|
||||||
|
!r = dkLen - (l - 1) * hLen
|
||||||
|
chunks = [f i | i <- [1 .. l]]
|
||||||
|
in (B.concat . init $ chunks) `B.append` B.take r (last chunks)
|
||||||
|
|
||||||
|
-- The @f@ function, as defined in the spec.
|
||||||
|
-- It calls 'u' under the hood.
|
||||||
|
f :: Int -> ByteString
|
||||||
|
f i = let !u1 = hmacSHA256 password (salt `B.append` int i)
|
||||||
|
-- Using the ST Monad, for maximum performance.
|
||||||
|
in runST $ do
|
||||||
|
u <- newSTRef u1
|
||||||
|
accum <- newSTRef u1
|
||||||
|
forM_ [2 .. c] $ \_ -> do
|
||||||
|
modifySTRef' u (hmacSHA256 password)
|
||||||
|
currentU <- readSTRef u
|
||||||
|
modifySTRef' accum (`xor'` currentU)
|
||||||
|
readSTRef accum
|
||||||
|
|
||||||
|
-- int(i), as defined in the spec.
|
||||||
|
int :: Int -> ByteString
|
||||||
|
int i = let str = BL.unpack . Binary.encode $ i
|
||||||
|
in BS.pack $ drop (length str - 4) str
|
||||||
|
|
||||||
|
-- | A convenience function to XOR two 'ByteString' together.
|
||||||
|
xor' :: ByteString -> ByteString -> ByteString
|
||||||
|
xor' !b1 !b2 = BS.pack $ BS.zipWith xor b1 b2
|
||||||
|
|
||||||
|
-- | Generate a 'Salt' from 128 bits of data from @\/dev\/urandom@, with the
|
||||||
|
-- system RNG as a fallback. This is the function used to generate salts by
|
||||||
|
-- 'makePassword'.
|
||||||
|
genSaltIO :: IO Salt
|
||||||
|
genSaltIO =
|
||||||
|
Control.Exception.catch genSaltDevURandom def
|
||||||
|
where
|
||||||
|
def :: IOError -> IO Salt
|
||||||
|
def _ = genSaltSysRandom
|
||||||
|
|
||||||
|
-- | Generate a 'Salt' from @\/dev\/urandom@.
|
||||||
|
genSaltDevURandom :: IO Salt
|
||||||
|
genSaltDevURandom = withFile "/dev/urandom" ReadMode $ \h -> do
|
||||||
|
rawSalt <- B.hGet h 16
|
||||||
|
return $ makeSalt rawSalt
|
||||||
|
|
||||||
|
-- | Generate a 'Salt' from 'System.Random'.
|
||||||
|
genSaltSysRandom :: IO Salt
|
||||||
|
genSaltSysRandom = randomChars >>= return . makeSalt . B.pack
|
||||||
|
where randomChars = sequence $ replicate 16 $ randomRIO ('\NUL', '\255')
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Password hash format
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- Format: "sha256|strength|salt|hash", where strength is an unsigned int, salt
|
||||||
|
-- is a base64-encoded 16-byte random number, and hash is a base64-encoded hash
|
||||||
|
-- value.
|
||||||
|
|
||||||
|
-- | Try to parse a password hash.
|
||||||
|
readPwHash :: ByteString -> Maybe (Int, Salt, ByteString)
|
||||||
|
readPwHash pw | length broken /= 4
|
||||||
|
|| algorithm /= "sha256"
|
||||||
|
|| B.length hash /= 44 = Nothing
|
||||||
|
| otherwise = case B.readInt strBS of
|
||||||
|
Just (strength, _) -> Just (strength, SaltBS salt, hash)
|
||||||
|
Nothing -> Nothing
|
||||||
|
where broken = B.split '|' pw
|
||||||
|
[algorithm, strBS, salt, hash] = broken
|
||||||
|
|
||||||
|
-- | Encode a password hash, from a @(strength, salt, hash)@ tuple, where
|
||||||
|
-- strength is an 'Int', and both @salt@ and @hash@ are base64-encoded
|
||||||
|
-- 'ByteString's.
|
||||||
|
writePwHash :: (Int, Salt, ByteString) -> ByteString
|
||||||
|
writePwHash (strength, SaltBS salt, hash) =
|
||||||
|
B.intercalate "|" ["sha256", B.pack (show strength), salt, hash]
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- High level API
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
-- | Hash a password with a given strength (14 is a good default). The output of
|
||||||
|
-- this function can be written directly to a password file or
|
||||||
|
-- database. Generates a salt using high-quality randomness from
|
||||||
|
-- @\/dev\/urandom@ or (if that is not available, for example on Windows)
|
||||||
|
-- 'System.Random', which is included in the hashed output.
|
||||||
|
makePassword :: ByteString -> Int -> IO ByteString
|
||||||
|
makePassword = makePasswordWith pbkdf1
|
||||||
|
|
||||||
|
-- | A generic version of 'makePassword', which allow the user
|
||||||
|
-- to choose the algorithm to use.
|
||||||
|
--
|
||||||
|
-- >>> makePasswordWith pbkdf1 "password" 14
|
||||||
|
--
|
||||||
|
makePasswordWith :: (ByteString -> Salt -> Int -> ByteString)
|
||||||
|
-- ^ The algorithm to use (e.g. pbkdf1)
|
||||||
|
-> ByteString
|
||||||
|
-- ^ The password to encrypt
|
||||||
|
-> Int
|
||||||
|
-- ^ log2 of the number of iterations
|
||||||
|
-> IO ByteString
|
||||||
|
makePasswordWith algorithm password strength = do
|
||||||
|
salt <- genSaltIO
|
||||||
|
return $ makePasswordSaltWith algorithm (2^) password salt strength
|
||||||
|
|
||||||
|
-- | A generic version of 'makePasswordSalt', meant to give the user
|
||||||
|
-- the maximum control over the generation parameters.
|
||||||
|
-- Note that, unlike 'makePasswordWith', this function takes the @raw@
|
||||||
|
-- number of iterations. This means the user will need to specify a
|
||||||
|
-- sensible value, typically @10000@ or @20000@.
|
||||||
|
makePasswordSaltWith :: (ByteString -> Salt -> Int -> ByteString)
|
||||||
|
-- ^ A function modeling an algorithm (e.g. 'pbkdf1')
|
||||||
|
-> (Int -> Int)
|
||||||
|
-- ^ A function to modify the strength
|
||||||
|
-> ByteString
|
||||||
|
-- ^ A password, given as clear text
|
||||||
|
-> Salt
|
||||||
|
-- ^ A hash 'Salt'
|
||||||
|
-> Int
|
||||||
|
-- ^ The password strength (e.g. @10000, 20000, etc.@)
|
||||||
|
-> ByteString
|
||||||
|
makePasswordSaltWith algorithm strengthModifier pwd salt strength = writePwHash (strength, salt, hash)
|
||||||
|
where hash = encode $ algorithm pwd salt (strengthModifier strength)
|
||||||
|
|
||||||
|
-- | Hash a password with a given strength (14 is a good default), using a given
|
||||||
|
-- salt. The output of this function can be written directly to a password file
|
||||||
|
-- or database. Example:
|
||||||
|
--
|
||||||
|
-- > >>> makePasswordSalt "hunter2" (makeSalt "72cd18b5ebfe6e96") 14
|
||||||
|
-- > "sha256|14|NzJjZDE4YjVlYmZlNmU5Ng==|yuiNrZW3KHX+pd0sWy9NTTsy5Yopmtx4UYscItSsoxc="
|
||||||
|
makePasswordSalt :: ByteString -> Salt -> Int -> ByteString
|
||||||
|
makePasswordSalt = makePasswordSaltWith pbkdf1 (2^)
|
||||||
|
|
||||||
|
-- | 'verifyPasswordWith' @algorithm userInput pwHash@ verifies
|
||||||
|
-- the password @userInput@ given by the user against the stored password
|
||||||
|
-- hash @pwHash@, with the hashing algorithm @algorithm@. Returns 'True' if the
|
||||||
|
-- given password is correct, and 'False' if it is not.
|
||||||
|
-- This function allows the programmer to specify the algorithm to use,
|
||||||
|
-- e.g. 'pbkdf1' or 'pbkdf2'.
|
||||||
|
-- Note: If you want to verify a password previously generated with
|
||||||
|
-- 'makePasswordSaltWith', but without modifying the number of iterations,
|
||||||
|
-- you can do:
|
||||||
|
--
|
||||||
|
-- > >>> verifyPasswordWith pbkdf2 id "hunter2" "sha256..."
|
||||||
|
-- > True
|
||||||
|
--
|
||||||
|
verifyPasswordWith :: (ByteString -> Salt -> Int -> ByteString)
|
||||||
|
-- ^ A function modeling an algorithm (e.g. pbkdf1)
|
||||||
|
-> (Int -> Int)
|
||||||
|
-- ^ A function to modify the strength
|
||||||
|
-> ByteString
|
||||||
|
-- ^ User password
|
||||||
|
-> ByteString
|
||||||
|
-- ^ The generated hash (e.g. sha256|14...)
|
||||||
|
-> Bool
|
||||||
|
verifyPasswordWith algorithm strengthModifier userInput pwHash =
|
||||||
|
case readPwHash pwHash of
|
||||||
|
Nothing -> False
|
||||||
|
Just (strength, salt, goodHash) ->
|
||||||
|
encode (algorithm userInput salt (strengthModifier strength)) == goodHash
|
||||||
|
|
||||||
|
-- | Like 'verifyPasswordWith', but uses 'pbkdf1' as algorithm.
|
||||||
|
verifyPassword :: ByteString -> ByteString -> Bool
|
||||||
|
verifyPassword = verifyPasswordWith pbkdf1 (2^)
|
||||||
|
|
||||||
|
-- | Try to strengthen a password hash, by hashing it some more
|
||||||
|
-- times. @'strengthenPassword' pwHash new_strength@ will return a new password
|
||||||
|
-- hash with strength at least @new_strength@. If the password hash already has
|
||||||
|
-- strength greater than or equal to @new_strength@, then it is returned
|
||||||
|
-- unmodified. If the password hash is invalid and does not parse, it will be
|
||||||
|
-- returned without comment.
|
||||||
|
--
|
||||||
|
-- This function can be used to periodically update your password database when
|
||||||
|
-- computers get faster, in order to keep up with Moore's law. This isn't hugely
|
||||||
|
-- important, but it's a good idea.
|
||||||
|
strengthenPassword :: ByteString -> Int -> ByteString
|
||||||
|
strengthenPassword pwHash newstr =
|
||||||
|
case readPwHash pwHash of
|
||||||
|
Nothing -> pwHash
|
||||||
|
Just (oldstr, salt, hashB64) ->
|
||||||
|
if oldstr < newstr then
|
||||||
|
writePwHash (newstr, salt, newHash)
|
||||||
|
else
|
||||||
|
pwHash
|
||||||
|
where newHash = encode $ hashRounds hash extraRounds
|
||||||
|
extraRounds = (2^newstr) - (2^oldstr)
|
||||||
|
hash = decodeLenient hashB64
|
||||||
|
|
||||||
|
-- | Return the strength of a password hash.
|
||||||
|
passwordStrength :: ByteString -> Int
|
||||||
|
passwordStrength pwHash = case readPwHash pwHash of
|
||||||
|
Nothing -> 0
|
||||||
|
Just (strength, _, _) -> strength
|
||||||
|
|
||||||
|
------------
|
||||||
|
-- Utilities
|
||||||
|
------------
|
||||||
|
|
||||||
|
-- | A salt is a unique random value which is stored as part of the password
|
||||||
|
-- hash. You can generate a salt with 'genSaltIO' or 'genSaltRandom', or if you
|
||||||
|
-- really know what you're doing, you can create them from your own ByteString
|
||||||
|
-- values with 'makeSalt'.
|
||||||
|
newtype Salt = SaltBS ByteString
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
-- | Create a 'Salt' from a 'ByteString'. The input must be at least 8
|
||||||
|
-- characters, and can contain arbitrary bytes. Most users will not need to use
|
||||||
|
-- this function.
|
||||||
|
makeSalt :: ByteString -> Salt
|
||||||
|
makeSalt = SaltBS . encode . check_length
|
||||||
|
where check_length salt | B.length salt < 8 =
|
||||||
|
error "Salt too short. Minimum length is 8 characters."
|
||||||
|
| otherwise = salt
|
||||||
|
|
||||||
|
-- | Convert a 'Salt' into a 'ByteString'. The resulting 'ByteString' will be
|
||||||
|
-- base64-encoded. Most users will not need to use this function.
|
||||||
|
exportSalt :: Salt -> ByteString
|
||||||
|
exportSalt (SaltBS bs) = bs
|
||||||
|
|
||||||
|
-- | Convert a raw 'ByteString' into a 'Salt'.
|
||||||
|
-- Use this function with caution, since using a weak salt will result in a
|
||||||
|
-- weak password.
|
||||||
|
importSalt :: ByteString -> Salt
|
||||||
|
importSalt = SaltBS
|
||||||
|
|
||||||
|
-- | Is the format of a password hash valid? Attempts to parse a given password
|
||||||
|
-- hash. Returns 'True' if it parses correctly, and 'False' otherwise.
|
||||||
|
isPasswordFormatValid :: ByteString -> Bool
|
||||||
|
isPasswordFormatValid = isJust . readPwHash
|
||||||
|
|
||||||
|
-- | Generate a 'Salt' with 128 bits of data taken from a given random number
|
||||||
|
-- generator. Returns the salt and the updated random number generator. This is
|
||||||
|
-- meant to be used with 'makePasswordSalt' by people who would prefer to either
|
||||||
|
-- use their own random number generator or avoid the 'IO' monad.
|
||||||
|
genSaltRandom :: (RandomGen b) => b -> (Salt, b)
|
||||||
|
genSaltRandom gen = (salt, newgen)
|
||||||
|
where rands _ 0 = []
|
||||||
|
rands g n = (a, g') : rands g' (n-1 :: Int)
|
||||||
|
where (a, g') = randomR ('\NUL', '\255') g
|
||||||
|
salt = makeSalt $ B.pack $ map fst (rands gen 16)
|
||||||
|
newgen = snd $ last (rands gen 16)
|
||||||
|
|
||||||
|
#if !MIN_VERSION_base(4, 6, 0)
|
||||||
|
-- | Strict version of 'modifySTRef'
|
||||||
|
modifySTRef' :: STRef s a -> (a -> a) -> ST s ()
|
||||||
|
modifySTRef' ref f = do
|
||||||
|
x <- readSTRef ref
|
||||||
|
let x' = f x
|
||||||
|
x' `seq` writeSTRef ref x'
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MIN_VERSION_bytestring(0, 10, 0)
|
||||||
|
toStrict :: BL.ByteString -> BS.ByteString
|
||||||
|
toStrict = BL.toStrict
|
||||||
|
|
||||||
|
fromStrict :: BS.ByteString -> BL.ByteString
|
||||||
|
fromStrict = BL.fromStrict
|
||||||
|
#else
|
||||||
|
toStrict :: BL.ByteString -> BS.ByteString
|
||||||
|
toStrict = BS.concat . BL.toChunks
|
||||||
|
|
||||||
|
fromStrict :: BS.ByteString -> BL.ByteString
|
||||||
|
fromStrict = BL.fromChunks . return
|
||||||
|
#endif
|
||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-auth
|
name: yesod-auth
|
||||||
version: 1.2.1
|
version: 1.3.1.1
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman, Patrick Brisbin
|
author: Michael Snoyman, Patrick Brisbin
|
||||||
@ -10,9 +10,14 @@ stability: Stable
|
|||||||
cabal-version: >= 1.6.0
|
cabal-version: >= 1.6.0
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
homepage: http://www.yesodweb.com/
|
homepage: http://www.yesodweb.com/
|
||||||
description: This package provides a pluggable mechanism for allowing users to authenticate with your site. It comes with a number of common plugins, such as OpenID, BrowserID (a.k.a., Mozilla Persona), and email. Other packages are available from Hackage as well. If you've written such an add-on, please notify me so that it can be added to this description.
|
description:
|
||||||
.
|
This package provides a pluggable mechanism for allowing users to authenticate with your site. It comes with a number of common plugins, such as OpenID, BrowserID (a.k.a., Mozilla Persona), and email. Other packages are available from Hackage as well. If you've written such an add-on, please notify me so that it can be added to this description.
|
||||||
* <http://hackage.haskell.org/package/yesod-auth-account>: An account authentication plugin for Yesod
|
.
|
||||||
|
* <http://hackage.haskell.org/package/yesod-auth-account>: An account authentication plugin for Yesod
|
||||||
|
.
|
||||||
|
* <http://hackage.haskell.org/package/yesod-auth-hashdb>: The HashDB module previously packaged in yesod-auth, now with stronger, but compatible, security.
|
||||||
|
.
|
||||||
|
* <https://github.com/ollieh/yesod-auth-bcrypt/>: An alternative to the HashDB module.
|
||||||
extra-source-files: persona_sign_in_blue.png
|
extra-source-files: persona_sign_in_blue.png
|
||||||
|
|
||||||
library
|
library
|
||||||
@ -22,24 +27,24 @@ library
|
|||||||
, yesod-core >= 1.2 && < 1.3
|
, yesod-core >= 1.2 && < 1.3
|
||||||
, wai >= 1.4
|
, wai >= 1.4
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, pureMD5 >= 2.0
|
, base16-bytestring
|
||||||
|
, cryptohash
|
||||||
, random >= 1.0.0.2
|
, random >= 1.0.0.2
|
||||||
, text >= 0.7
|
, text >= 0.7
|
||||||
, mime-mail >= 0.3
|
, mime-mail >= 0.3
|
||||||
, yesod-persistent >= 1.2
|
, yesod-persistent >= 1.2
|
||||||
, hamlet >= 1.1 && < 1.2
|
, hamlet >= 1.1
|
||||||
, shakespeare-css >= 1.0 && < 1.1
|
, shakespeare
|
||||||
, shakespeare-js >= 1.0.2 && < 1.2
|
, shakespeare-css >= 1.0
|
||||||
|
, shakespeare-js >= 1.0.2
|
||||||
, containers
|
, containers
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, yesod-form >= 1.3 && < 1.4
|
, yesod-form >= 1.3 && < 1.4
|
||||||
, transformers >= 0.2.2
|
, transformers >= 0.2.2
|
||||||
, persistent >= 1.2 && < 1.3
|
, persistent >= 1.2 && < 1.4
|
||||||
, persistent-template >= 1.2 && < 1.3
|
, persistent-template >= 1.2 && < 1.4
|
||||||
, SHA >= 1.4.1.3
|
|
||||||
, http-conduit >= 1.5
|
, http-conduit >= 1.5
|
||||||
, aeson >= 0.5
|
, aeson >= 0.5
|
||||||
, pwstore-fast >= 2.2
|
|
||||||
, lifted-base >= 0.1
|
, lifted-base >= 0.1
|
||||||
, blaze-html >= 0.5
|
, blaze-html >= 0.5
|
||||||
, blaze-markup >= 0.5.1
|
, blaze-markup >= 0.5.1
|
||||||
@ -51,6 +56,14 @@ library
|
|||||||
, resourcet
|
, resourcet
|
||||||
, safe
|
, safe
|
||||||
, time
|
, time
|
||||||
|
, base64-bytestring
|
||||||
|
, byteable
|
||||||
|
, binary
|
||||||
|
, http-client
|
||||||
|
, blaze-builder
|
||||||
|
, conduit
|
||||||
|
, conduit-extra
|
||||||
|
, attoparsec-conduit
|
||||||
|
|
||||||
exposed-modules: Yesod.Auth
|
exposed-modules: Yesod.Auth
|
||||||
Yesod.Auth.BrowserId
|
Yesod.Auth.BrowserId
|
||||||
@ -58,10 +71,11 @@ library
|
|||||||
Yesod.Auth.Email
|
Yesod.Auth.Email
|
||||||
Yesod.Auth.OpenId
|
Yesod.Auth.OpenId
|
||||||
Yesod.Auth.Rpxnow
|
Yesod.Auth.Rpxnow
|
||||||
Yesod.Auth.HashDB
|
|
||||||
Yesod.Auth.Message
|
Yesod.Auth.Message
|
||||||
Yesod.Auth.GoogleEmail
|
Yesod.Auth.GoogleEmail
|
||||||
|
Yesod.Auth.GoogleEmail2
|
||||||
other-modules: Yesod.Auth.Routes
|
other-modules: Yesod.Auth.Routes
|
||||||
|
Yesod.PasswordStore
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
|
|||||||
@ -106,7 +106,7 @@ mkHandler name pattern methods = unlines
|
|||||||
|
|
||||||
getTypes "" = []
|
getTypes "" = []
|
||||||
getTypes ('/':rest) = getTypes rest
|
getTypes ('/':rest) = getTypes rest
|
||||||
getTypes ('#':rest) =
|
getTypes (c:rest) | c `elem` "#*" =
|
||||||
typ : getTypes rest'
|
typ : getTypes rest'
|
||||||
where
|
where
|
||||||
(typ, rest') = break (== '/') rest
|
(typ, rest') = break (== '/') rest
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module Devel
|
module Devel
|
||||||
( devel
|
( devel
|
||||||
, DevelOpts(..)
|
, DevelOpts(..)
|
||||||
|
, DevelTermOpt(..)
|
||||||
, defaultDevelOpts
|
, defaultDevelOpts
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Paths_yesod_bin
|
|
||||||
|
|
||||||
import qualified Distribution.Compiler as D
|
import qualified Distribution.Compiler as D
|
||||||
import qualified Distribution.ModuleName as D
|
import qualified Distribution.ModuleName as D
|
||||||
import qualified Distribution.PackageDescription as D
|
import qualified Distribution.PackageDescription as D
|
||||||
@ -24,7 +24,7 @@ import Control.Concurrent.MVar (MVar, newEmptyMVar,
|
|||||||
takeMVar, tryPutMVar)
|
takeMVar, tryPutMVar)
|
||||||
import qualified Control.Exception as Ex
|
import qualified Control.Exception as Ex
|
||||||
import Control.Monad (forever, unless, void,
|
import Control.Monad (forever, unless, void,
|
||||||
when)
|
when, forM)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Trans.State (evalStateT, get)
|
import Control.Monad.Trans.State (evalStateT, get)
|
||||||
import qualified Data.IORef as I
|
import qualified Data.IORef as I
|
||||||
@ -64,20 +64,26 @@ import GhcBuild (buildPackage,
|
|||||||
getBuildFlags, getPackageArgs)
|
getBuildFlags, getPackageArgs)
|
||||||
|
|
||||||
import qualified Config as GHC
|
import qualified Config as GHC
|
||||||
import Data.Conduit.Network (HostPreference (HostIPv4),
|
import Data.Streaming.Network (bindPortTCP)
|
||||||
bindPort)
|
|
||||||
import Network (withSocketsDo)
|
import Network (withSocketsDo)
|
||||||
|
#if MIN_VERSION_http_conduit(2, 0, 0)
|
||||||
|
import Network.HTTP.Conduit (conduitManagerSettings, newManager)
|
||||||
|
import Data.Default.Class (def)
|
||||||
|
#else
|
||||||
import Network.HTTP.Conduit (def, newManager)
|
import Network.HTTP.Conduit (def, newManager)
|
||||||
|
#endif
|
||||||
import Network.HTTP.ReverseProxy (ProxyDest (ProxyDest),
|
import Network.HTTP.ReverseProxy (ProxyDest (ProxyDest),
|
||||||
waiProxyToSettings, wpsTimeout, wpsOnExc)
|
waiProxyToSettings, wpsTimeout, wpsOnExc)
|
||||||
#if MIN_VERSION_http_reverse_proxy(0, 2, 0)
|
#if MIN_VERSION_http_reverse_proxy(0, 2, 0)
|
||||||
import qualified Network.HTTP.ReverseProxy as ReverseProxy
|
import qualified Network.HTTP.ReverseProxy as ReverseProxy
|
||||||
#endif
|
#endif
|
||||||
import Network.HTTP.Types (status200)
|
import Network.HTTP.Types (status200, status503)
|
||||||
import Network.Socket (sClose)
|
import Network.Socket (sClose)
|
||||||
import Network.Wai (responseLBS)
|
import Network.Wai (responseLBS, requestHeaders)
|
||||||
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import Network.Wai.Handler.Warp (run)
|
import Network.Wai.Handler.Warp (run)
|
||||||
import SrcLoc (Located)
|
import SrcLoc (Located)
|
||||||
|
import Data.FileEmbed (embedFile)
|
||||||
|
|
||||||
lockFile :: DevelOpts -> FilePath
|
lockFile :: DevelOpts -> FilePath
|
||||||
lockFile _opts = "yesod-devel/devel-terminate"
|
lockFile _opts = "yesod-devel/devel-terminate"
|
||||||
@ -94,6 +100,8 @@ removeLock opts = do
|
|||||||
removeFileIfExists (lockFile opts)
|
removeFileIfExists (lockFile opts)
|
||||||
removeFileIfExists "dist/devel-terminate" -- for compatibility with old devel.hs
|
removeFileIfExists "dist/devel-terminate" -- for compatibility with old devel.hs
|
||||||
|
|
||||||
|
data DevelTermOpt = TerminateOnEnter | TerminateOnlyInterrupt
|
||||||
|
deriving (Show, Eq)
|
||||||
data DevelOpts = DevelOpts
|
data DevelOpts = DevelOpts
|
||||||
{ isCabalDev :: Bool
|
{ isCabalDev :: Bool
|
||||||
, forceCabal :: Bool
|
, forceCabal :: Bool
|
||||||
@ -105,13 +113,14 @@ data DevelOpts = DevelOpts
|
|||||||
, develPort :: Int
|
, develPort :: Int
|
||||||
, proxyTimeout :: Int
|
, proxyTimeout :: Int
|
||||||
, useReverseProxy :: Bool
|
, useReverseProxy :: Bool
|
||||||
|
, terminateWith :: DevelTermOpt
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
getBuildDir :: DevelOpts -> String
|
getBuildDir :: DevelOpts -> String
|
||||||
getBuildDir opts = fromMaybe "dist" (buildDir opts)
|
getBuildDir opts = fromMaybe "dist" (buildDir opts)
|
||||||
|
|
||||||
defaultDevelOpts :: DevelOpts
|
defaultDevelOpts :: DevelOpts
|
||||||
defaultDevelOpts = DevelOpts False False False (-1) Nothing Nothing Nothing 3000 10 True
|
defaultDevelOpts = DevelOpts False False False (-1) Nothing Nothing Nothing 3000 10 True TerminateOnEnter
|
||||||
|
|
||||||
cabalProgram :: DevelOpts -> FilePath
|
cabalProgram :: DevelOpts -> FilePath
|
||||||
cabalProgram opts | isCabalDev opts = "cabal-dev"
|
cabalProgram opts | isCabalDev opts = "cabal-dev"
|
||||||
@ -121,8 +130,26 @@ cabalProgram opts | isCabalDev opts = "cabal-dev"
|
|||||||
-- 3001, give an appropriate message to the user.
|
-- 3001, give an appropriate message to the user.
|
||||||
reverseProxy :: DevelOpts -> I.IORef Int -> IO ()
|
reverseProxy :: DevelOpts -> I.IORef Int -> IO ()
|
||||||
reverseProxy opts iappPort = do
|
reverseProxy opts iappPort = do
|
||||||
|
#if MIN_VERSION_http_conduit(2, 0, 0)
|
||||||
|
manager <- newManager conduitManagerSettings
|
||||||
|
#else
|
||||||
manager <- newManager def
|
manager <- newManager def
|
||||||
let loop = forever $ do
|
#endif
|
||||||
|
let refreshHtml = LB.fromChunks $ return $(embedFile "refreshing.html")
|
||||||
|
let onExc _ req
|
||||||
|
| maybe False (("application/json" `elem`) . parseHttpAccept)
|
||||||
|
(lookup "accept" $ requestHeaders req) =
|
||||||
|
return $ responseLBS status503
|
||||||
|
[ ("Retry-After", "1")
|
||||||
|
]
|
||||||
|
"{\"message\":\"Recompiling\"}"
|
||||||
|
| otherwise = return $ responseLBS status200
|
||||||
|
[ ("content-type", "text/html")
|
||||||
|
, ("Refresh", "1")
|
||||||
|
]
|
||||||
|
refreshHtml
|
||||||
|
|
||||||
|
let runProxy =
|
||||||
run (develPort opts) $ waiProxyToSettings
|
run (develPort opts) $ waiProxyToSettings
|
||||||
(const $ do
|
(const $ do
|
||||||
appPort <- liftIO $ I.readIORef iappPort
|
appPort <- liftIO $ I.readIORef iappPort
|
||||||
@ -134,31 +161,28 @@ reverseProxy opts iappPort = do
|
|||||||
#endif
|
#endif
|
||||||
$ ProxyDest "127.0.0.1" appPort)
|
$ ProxyDest "127.0.0.1" appPort)
|
||||||
def
|
def
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
{ wpsOnExc = \e req f -> onExc e req >>= f
|
||||||
|
#else
|
||||||
{ wpsOnExc = onExc
|
{ wpsOnExc = onExc
|
||||||
|
#endif
|
||||||
, wpsTimeout =
|
, wpsTimeout =
|
||||||
if proxyTimeout opts == 0
|
if proxyTimeout opts == 0
|
||||||
then Nothing
|
then Nothing
|
||||||
else Just (1000000 * proxyTimeout opts)
|
else Just (1000000 * proxyTimeout opts)
|
||||||
}
|
}
|
||||||
manager
|
manager
|
||||||
putStrLn "Reverse proxy stopped, but it shouldn't"
|
loop runProxy `Ex.onException` exitFailure
|
||||||
threadDelay 1000000
|
|
||||||
putStrLn "Restarting reverse proxy"
|
|
||||||
loop `Ex.onException` exitFailure
|
|
||||||
where
|
where
|
||||||
onExc _ _ = do
|
loop proxy = forever $ do
|
||||||
refreshing <- liftIO $ getDataFileName "refreshing.html"
|
void proxy
|
||||||
html <- liftIO $ LB.readFile refreshing
|
putStrLn "Reverse proxy stopped, but it shouldn't"
|
||||||
return $ responseLBS
|
threadDelay 1000000
|
||||||
status200
|
putStrLn "Restarting reverse proxy"
|
||||||
[ ("content-type", "text/html")
|
|
||||||
, ("Refresh", "1")
|
|
||||||
]
|
|
||||||
html
|
|
||||||
|
|
||||||
checkPort :: Int -> IO Bool
|
checkPort :: Int -> IO Bool
|
||||||
checkPort p = do
|
checkPort p = do
|
||||||
es <- Ex.try $ bindPort p HostIPv4
|
es <- Ex.try $ bindPortTCP p "*4"
|
||||||
case es of
|
case es of
|
||||||
Left (_ :: Ex.IOException) -> return False
|
Left (_ :: Ex.IOException) -> return False
|
||||||
Right s -> do
|
Right s -> do
|
||||||
@ -174,21 +198,31 @@ getPort _ p0 =
|
|||||||
avail <- checkPort p
|
avail <- checkPort p
|
||||||
if avail then return p else loop (succ p)
|
if avail then return p else loop (succ p)
|
||||||
|
|
||||||
|
unlessM :: Monad m => m Bool -> m () -> m ()
|
||||||
|
unlessM c a = c >>= \res -> unless res a
|
||||||
|
|
||||||
devel :: DevelOpts -> [String] -> IO ()
|
devel :: DevelOpts -> [String] -> IO ()
|
||||||
devel opts passThroughArgs = withSocketsDo $ withManager $ \manager -> do
|
devel opts passThroughArgs = withSocketsDo $ withManager $ \manager -> do
|
||||||
avail <- checkPort $ develPort opts
|
unlessM (checkPort $ develPort opts) $ error "devel port unavailable"
|
||||||
unless avail $ error "devel port unavailable"
|
|
||||||
iappPort <- getPort opts 17834 >>= I.newIORef
|
iappPort <- getPort opts 17834 >>= I.newIORef
|
||||||
when (useReverseProxy opts) $ void $ forkIO $ reverseProxy opts iappPort
|
when (useReverseProxy opts) $ void $ forkIO $ reverseProxy opts iappPort
|
||||||
checkDevelFile
|
checkDevelFile
|
||||||
writeLock opts
|
writeLock opts
|
||||||
|
|
||||||
putStrLn "Yesod devel server. Press ENTER to quit"
|
let (terminator, after) = case terminateWith opts of
|
||||||
_ <- forkIO $ do
|
TerminateOnEnter ->
|
||||||
|
("Press ENTER", void getLine)
|
||||||
|
TerminateOnlyInterrupt -> -- run for one year
|
||||||
|
("Interrupt", threadDelay $ 1000 * 1000 * 60 * 60 * 24 * 365)
|
||||||
|
|
||||||
|
|
||||||
|
putStrLn $ "Yesod devel server. " ++ terminator ++ " to quit"
|
||||||
|
void $ forkIO $ do
|
||||||
filesModified <- newEmptyMVar
|
filesModified <- newEmptyMVar
|
||||||
watchTree manager "." (const True) (\_ -> void (tryPutMVar filesModified ()))
|
void $ forkIO $
|
||||||
|
void $ watchTree manager "." (const True) (\_ -> void (tryPutMVar filesModified ()))
|
||||||
evalStateT (mainOuterLoop iappPort filesModified) Map.empty
|
evalStateT (mainOuterLoop iappPort filesModified) Map.empty
|
||||||
_ <- getLine
|
after
|
||||||
writeLock opts
|
writeLock opts
|
||||||
exitSuccess
|
exitSuccess
|
||||||
where
|
where
|
||||||
@ -247,7 +281,10 @@ devel opts passThroughArgs = withSocketsDo $ withManager $ \manager -> do
|
|||||||
liftIO $ I.writeIORef iappPort appPort
|
liftIO $ I.writeIORef iappPort appPort
|
||||||
|
|
||||||
(_,_,_,ph) <- liftIO $ createProcess (proc "runghc" devArgs)
|
(_,_,_,ph) <- liftIO $ createProcess (proc "runghc" devArgs)
|
||||||
{ env = Just $ ("PORT", show appPort) : ("DISPLAY_PORT", show $ develPort opts) : env0
|
{ env = Just $ Map.toList
|
||||||
|
$ Map.insert "PORT" (show appPort)
|
||||||
|
$ Map.insert "DISPLAY_PORT" (show $ develPort opts)
|
||||||
|
$ Map.fromList env0
|
||||||
}
|
}
|
||||||
derefMap <- get
|
derefMap <- get
|
||||||
watchTid <- liftIO . forkIO . try_ $ flip evalStateT derefMap $ do
|
watchTid <- liftIO . forkIO . try_ $ flip evalStateT derefMap $ do
|
||||||
@ -278,8 +315,8 @@ runBuildHook Nothing = return ()
|
|||||||
-}
|
-}
|
||||||
configure :: DevelOpts -> [String] -> IO Bool
|
configure :: DevelOpts -> [String] -> IO Bool
|
||||||
configure opts extraArgs =
|
configure opts extraArgs =
|
||||||
checkExit =<< (createProcess $ proc (cabalProgram opts)
|
checkExit =<< createProcess (proc (cabalProgram opts) $
|
||||||
([ "configure"
|
[ "configure"
|
||||||
, "-flibrary-only"
|
, "-flibrary-only"
|
||||||
, "-fdevel"
|
, "-fdevel"
|
||||||
, "--disable-library-profiling"
|
, "--disable-library-profiling"
|
||||||
@ -287,7 +324,7 @@ configure opts extraArgs =
|
|||||||
, "--with-ghc=yesod-ghc-wrapper"
|
, "--with-ghc=yesod-ghc-wrapper"
|
||||||
, "--with-ar=yesod-ar-wrapper"
|
, "--with-ar=yesod-ar-wrapper"
|
||||||
, "--with-hc-pkg=ghc-pkg"
|
, "--with-hc-pkg=ghc-pkg"
|
||||||
] ++ extraArgs)
|
] ++ extraArgs
|
||||||
)
|
)
|
||||||
|
|
||||||
removeFileIfExists :: FilePath -> IO ()
|
removeFileIfExists :: FilePath -> IO ()
|
||||||
@ -302,7 +339,7 @@ mkRebuild ghcVer cabalFile opts (ldPath, arPath)
|
|||||||
| GHC.cProjectVersion /= ghcVer =
|
| GHC.cProjectVersion /= ghcVer =
|
||||||
failWith "Yesod has been compiled with a different GHC version, please reinstall"
|
failWith "Yesod has been compiled with a different GHC version, please reinstall"
|
||||||
| forceCabal opts = return (rebuildCabal opts)
|
| forceCabal opts = return (rebuildCabal opts)
|
||||||
| otherwise = do
|
| otherwise =
|
||||||
return $ do
|
return $ do
|
||||||
ns <- mapM (cabalFile `isNewerThan`)
|
ns <- mapM (cabalFile `isNewerThan`)
|
||||||
[ "yesod-devel/ghcargs.txt", "yesod-devel/arargs.txt", "yesod-devel/ldargs.txt" ]
|
[ "yesod-devel/ghcargs.txt", "yesod-devel/arargs.txt", "yesod-devel/ldargs.txt" ]
|
||||||
@ -327,7 +364,7 @@ rebuildCabal opts = do
|
|||||||
| otherwise = [ "build", "-v0" ]
|
| otherwise = [ "build", "-v0" ]
|
||||||
|
|
||||||
try_ :: forall a. IO a -> IO ()
|
try_ :: forall a. IO a -> IO ()
|
||||||
try_ x = (Ex.try x :: IO (Either Ex.SomeException a)) >> return ()
|
try_ x = void (Ex.try x :: IO (Either Ex.SomeException a))
|
||||||
|
|
||||||
type FileList = Map.Map FilePath EpochTime
|
type FileList = Map.Map FilePath EpochTime
|
||||||
|
|
||||||
@ -335,7 +372,7 @@ getFileList :: [FilePath] -> [FilePath] -> IO FileList
|
|||||||
getFileList hsSourceDirs extraFiles = do
|
getFileList hsSourceDirs extraFiles = do
|
||||||
(files, deps) <- getDeps hsSourceDirs
|
(files, deps) <- getDeps hsSourceDirs
|
||||||
let files' = extraFiles ++ files ++ map fst (Map.toList deps)
|
let files' = extraFiles ++ files ++ map fst (Map.toList deps)
|
||||||
fmap Map.fromList $ flip mapM files' $ \f -> do
|
fmap Map.fromList $ forM files' $ \f -> do
|
||||||
efs <- Ex.try $ getFileStatus f
|
efs <- Ex.try $ getFileStatus f
|
||||||
return $ case efs of
|
return $ case efs of
|
||||||
Left (_ :: Ex.SomeException) -> (f, 0)
|
Left (_ :: Ex.SomeException) -> (f, 0)
|
||||||
@ -389,7 +426,7 @@ failWith msg = do
|
|||||||
exitFailure
|
exitFailure
|
||||||
|
|
||||||
checkFileList :: FileList -> D.Library -> [FilePath]
|
checkFileList :: FileList -> D.Library -> [FilePath]
|
||||||
checkFileList fl lib = filter isUnlisted . filter isSrcFile $ sourceFiles
|
checkFileList fl lib = filter (not . isSetup) . filter isUnlisted . filter isSrcFile $ sourceFiles
|
||||||
where
|
where
|
||||||
al = allModules lib
|
al = allModules lib
|
||||||
-- a file is only a possible 'module file' if all path pieces start with a capital letter
|
-- a file is only a possible 'module file' if all path pieces start with a capital letter
|
||||||
@ -399,6 +436,12 @@ checkFileList fl lib = filter isUnlisted . filter isSrcFile $ sourceFiles
|
|||||||
isUnlisted file = not (toModuleName file `Set.member` al)
|
isUnlisted file = not (toModuleName file `Set.member` al)
|
||||||
toModuleName = L.intercalate "." . filter (/=".") . splitDirectories . dropExtension
|
toModuleName = L.intercalate "." . filter (/=".") . splitDirectories . dropExtension
|
||||||
|
|
||||||
|
isSetup "Setup.hs" = True
|
||||||
|
isSetup "./Setup.hs" = True
|
||||||
|
isSetup "Setup.lhs" = True
|
||||||
|
isSetup "./Setup.lhs" = True
|
||||||
|
isSetup _ = False
|
||||||
|
|
||||||
allModules :: D.Library -> Set.Set String
|
allModules :: D.Library -> Set.Set String
|
||||||
allModules lib = Set.fromList $ map toString $ D.exposedModules lib ++ (D.otherModules . D.libBuildInfo) lib
|
allModules lib = Set.fromList $ map toString $ D.exposedModules lib ++ (D.otherModules . D.libBuildInfo) lib
|
||||||
where
|
where
|
||||||
|
|||||||
@ -39,10 +39,14 @@ import GHC.Paths (libdir)
|
|||||||
import HscTypes (HscEnv (..), emptyHomePackageTable)
|
import HscTypes (HscEnv (..), emptyHomePackageTable)
|
||||||
import qualified Module
|
import qualified Module
|
||||||
import MonadUtils (liftIO)
|
import MonadUtils (liftIO)
|
||||||
import Panic (ghcError, panic)
|
import Panic (throwGhcException, panic)
|
||||||
import SrcLoc (Located, mkGeneralLocated)
|
import SrcLoc (Located, mkGeneralLocated)
|
||||||
import qualified StaticFlags
|
import qualified StaticFlags
|
||||||
|
#if __GLASGOW_HASKELL__ >= 707
|
||||||
|
import DynFlags (ldInputs)
|
||||||
|
#else
|
||||||
import StaticFlags (v_Ld_inputs)
|
import StaticFlags (v_Ld_inputs)
|
||||||
|
#endif
|
||||||
import System.FilePath (normalise, (</>))
|
import System.FilePath (normalise, (</>))
|
||||||
import Util (consIORef, looksLikeModuleName)
|
import Util (consIORef, looksLikeModuleName)
|
||||||
|
|
||||||
@ -147,7 +151,9 @@ buildPackage' argv2 ld ar = do
|
|||||||
haskellish (f,Nothing) =
|
haskellish (f,Nothing) =
|
||||||
looksLikeModuleName f || isHaskellSrcFilename f || '.' `notElem` f
|
looksLikeModuleName f || isHaskellSrcFilename f || '.' `notElem` f
|
||||||
haskellish (_,Just phase) =
|
haskellish (_,Just phase) =
|
||||||
#if MIN_VERSION_ghc(7,4,0)
|
#if MIN_VERSION_ghc(7,8,3)
|
||||||
|
phase `notElem` [As True, As False, Cc, Cobjc, Cobjcpp, CmmCpp, Cmm, StopLn]
|
||||||
|
#elif MIN_VERSION_ghc(7,4,0)
|
||||||
phase `notElem` [As, Cc, Cobjc, Cobjcpp, CmmCpp, Cmm, StopLn]
|
phase `notElem` [As, Cc, Cobjc, Cobjcpp, CmmCpp, Cmm, StopLn]
|
||||||
#else
|
#else
|
||||||
phase `notElem` [As, Cc, CmmCpp, Cmm, StopLn]
|
phase `notElem` [As, Cc, CmmCpp, Cmm, StopLn]
|
||||||
@ -162,7 +168,15 @@ buildPackage' argv2 ld ar = do
|
|||||||
o_files <- mapM (\x -> compileFile hsc_env StopLn x)
|
o_files <- mapM (\x -> compileFile hsc_env StopLn x)
|
||||||
#endif
|
#endif
|
||||||
non_hs_srcs
|
non_hs_srcs
|
||||||
|
#if __GLASGOW_HASKELL__ >= 707
|
||||||
|
let dflags4 = dflags3
|
||||||
|
{ ldInputs = map (DF.FileOption "") (reverse o_files)
|
||||||
|
++ ldInputs dflags3
|
||||||
|
}
|
||||||
|
GHC.setSessionDynFlags dflags4
|
||||||
|
#else
|
||||||
liftIO $ mapM_ (consIORef v_Ld_inputs) (reverse o_files)
|
liftIO $ mapM_ (consIORef v_Ld_inputs) (reverse o_files)
|
||||||
|
#endif
|
||||||
targets <- mapM (uncurry GHC.guessTarget) hs_srcs
|
targets <- mapM (uncurry GHC.guessTarget) hs_srcs
|
||||||
GHC.setTargets targets
|
GHC.setTargets targets
|
||||||
ok_flag <- GHC.load GHC.LoadAllTargets
|
ok_flag <- GHC.load GHC.LoadAllTargets
|
||||||
@ -234,7 +248,7 @@ parseModeFlags args = do
|
|||||||
Nothing -> doMakeMode
|
Nothing -> doMakeMode
|
||||||
Just (m, _) -> m
|
Just (m, _) -> m
|
||||||
errs = errs1 ++ map (mkGeneralLocated "on the commandline") errs2
|
errs = errs1 ++ map (mkGeneralLocated "on the commandline") errs2
|
||||||
when (not (null errs)) $ ghcError $ errorsToGhcException errs
|
when (not (null errs)) $ throwGhcException $ errorsToGhcException errs
|
||||||
return (mode, flags' ++ leftover, warns)
|
return (mode, flags' ++ leftover, warns)
|
||||||
|
|
||||||
type ModeM = CmdLineP (Maybe (Mode, String), [String], [Located String])
|
type ModeM = CmdLineP (Maybe (Mode, String), [String], [Located String])
|
||||||
@ -289,7 +303,11 @@ mode_flags =
|
|||||||
, Flag "E" (PassFlag (setMode (stopBeforeMode anyHsc)))
|
, Flag "E" (PassFlag (setMode (stopBeforeMode anyHsc)))
|
||||||
, Flag "C" (PassFlag (\f -> do setMode (stopBeforeMode HCc) f
|
, Flag "C" (PassFlag (\f -> do setMode (stopBeforeMode HCc) f
|
||||||
addFlag "-fvia-C" f))
|
addFlag "-fvia-C" f))
|
||||||
|
#if MIN_VERSION_ghc(7,8,3)
|
||||||
|
, Flag "S" (PassFlag (setMode (stopBeforeMode (As True))))
|
||||||
|
#else
|
||||||
, Flag "S" (PassFlag (setMode (stopBeforeMode As)))
|
, Flag "S" (PassFlag (setMode (stopBeforeMode As)))
|
||||||
|
#endif
|
||||||
, Flag "-make" (PassFlag (setMode doMakeMode))
|
, Flag "-make" (PassFlag (setMode doMakeMode))
|
||||||
, Flag "-interactive" (PassFlag (setMode doInteractiveMode))
|
, Flag "-interactive" (PassFlag (setMode doInteractiveMode))
|
||||||
, Flag "-abi-hash" (PassFlag (setMode doAbiHashMode))
|
, Flag "-abi-hash" (PassFlag (setMode doAbiHashMode))
|
||||||
|
|||||||
38
yesod-bin/HsFile.hs
Normal file
38
yesod-bin/HsFile.hs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module HsFile (mkHsFile) where
|
||||||
|
import Text.ProjectTemplate (createTemplate)
|
||||||
|
import Data.Conduit
|
||||||
|
( ($$), (=$), ConduitM, awaitForever, yield, Source )
|
||||||
|
import Control.Monad.Trans.Resource (ResourceT, runResourceT)
|
||||||
|
import qualified Data.Conduit.List as CL
|
||||||
|
import Prelude hiding (FilePath)
|
||||||
|
import Filesystem.Path ( FilePath )
|
||||||
|
import Filesystem.Path.CurrentOS ( encodeString )
|
||||||
|
import qualified Filesystem as F
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
|
||||||
|
traverse :: FilePath -> Source (ResourceT IO) FilePath
|
||||||
|
traverse dir = do
|
||||||
|
liftIO (F.listDirectory dir) >>= mapM_ go
|
||||||
|
where
|
||||||
|
go fp = do
|
||||||
|
isFile' <- liftIO $ F.isFile fp
|
||||||
|
if isFile'
|
||||||
|
then yield fp
|
||||||
|
else do
|
||||||
|
isDir <- liftIO $ F.isDirectory fp
|
||||||
|
if isDir
|
||||||
|
then traverse fp
|
||||||
|
else return ()
|
||||||
|
|
||||||
|
mkHsFile :: IO ()
|
||||||
|
mkHsFile = runResourceT $ traverse "."
|
||||||
|
$$ readIt
|
||||||
|
=$ createTemplate
|
||||||
|
=$ awaitForever (liftIO . BS.putStr)
|
||||||
|
|
||||||
|
-- Reads a filepath from upstream and dumps a pair of (filepath, filecontents)
|
||||||
|
readIt :: ConduitM FilePath (FilePath, ResourceT IO BS.ByteString) (ResourceT IO) ()
|
||||||
|
readIt = CL.map $ \i -> (i, liftIO $ BS.readFile $ encodeString i)
|
||||||
|
|
||||||
@ -62,7 +62,10 @@ keter cabal noBuild = do
|
|||||||
L.writeFile fp $ compress $ Tar.write archive
|
L.writeFile fp $ compress $ Tar.write archive
|
||||||
|
|
||||||
case Map.lookup "copy-to" value of
|
case Map.lookup "copy-to" value of
|
||||||
Just (String s) -> run "scp" [fp, T.unpack s]
|
Just (String s) ->
|
||||||
|
case parseMaybe (.: "copy-to-port") value of
|
||||||
|
Just i -> run "scp" ["-P" ++ show (i :: Int), fp, T.unpack s]
|
||||||
|
Nothing -> run "scp" [fp, T.unpack s]
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
try' :: IO a -> IO (Either SomeException a)
|
try' :: IO a -> IO (Either SomeException a)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import Data.Char (isAlphaNum, isSpace, toLower)
|
|||||||
import Data.List (foldl')
|
import Data.List (foldl')
|
||||||
import Data.List.Split (splitOn)
|
import Data.List.Split (splitOn)
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
import Data.Maybe (catMaybes)
|
import Data.Maybe (mapMaybe)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Options.Applicative.Types
|
import Options.Applicative.Types
|
||||||
@ -52,10 +52,10 @@ updateA env key upd a =
|
|||||||
|
|
||||||
-- | really simple key/value file reader: x.y = z -> (["x","y"],"z")
|
-- | really simple key/value file reader: x.y = z -> (["x","y"],"z")
|
||||||
configLines :: String -> [([String], String)]
|
configLines :: String -> [([String], String)]
|
||||||
configLines = catMaybes . map (mkLine . takeWhile (/='#')) . lines
|
configLines = mapMaybe (mkLine . takeWhile (/='#')) . lines
|
||||||
where
|
where
|
||||||
trim = let f = reverse . dropWhile isSpace in f . f
|
trim = let f = reverse . dropWhile isSpace in f . f
|
||||||
mkLine l | (k, ('=':v)) <- break (=='=') l = Just (splitOn "." (trim k), trim v)
|
mkLine l | (k, '=':v) <- break (=='=') l = Just (splitOn "." (trim k), trim v)
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
|
|
||||||
-- | inject the environment into the parser
|
-- | inject the environment into the parser
|
||||||
@ -71,16 +71,27 @@ injectDefaultP env path p@(OptP o)
|
|||||||
in parseri { infoParser = injectDefaultP env (path ++ [normalizeName cmd]) (infoParser parseri) }
|
in parseri { infoParser = injectDefaultP env (path ++ [normalizeName cmd]) (infoParser parseri) }
|
||||||
in OptP (Option (CmdReader cmds (`M.lookup` cmdMap)) props)
|
in OptP (Option (CmdReader cmds (`M.lookup` cmdMap)) props)
|
||||||
| (Option (OptReader names (CReader _ rdr) _) _) <- o =
|
| (Option (OptReader names (CReader _ rdr) _) _) <- o =
|
||||||
p <|> either (const empty) pure (msum $ map (rdr <=< (maybe (Left $ ErrorMsg "Missing environment variable") Right . getEnvValue env path)) names)
|
p <|> either' (const empty) pure (msum $ map (rdr <=< (maybe (left $ ErrorMsg "Missing environment variable") right . getEnvValue env path)) names)
|
||||||
| (Option (FlagReader names a) _) <- o =
|
| (Option (FlagReader names a) _) <- o =
|
||||||
p <|> if any ((==Just "1") . getEnvValue env path) names then pure a else empty
|
p <|> if any ((==Just "1") . getEnvValue env path) names then pure a else empty
|
||||||
| otherwise = p
|
| otherwise = p
|
||||||
|
where
|
||||||
|
#if MIN_VERSION_optparse_applicative(0,6,0)
|
||||||
|
right= ReadM . Right
|
||||||
|
left = ReadM . Left
|
||||||
|
either' f g (ReadM x) = either f g x
|
||||||
|
#else
|
||||||
|
right = Right
|
||||||
|
left = Left
|
||||||
|
either' = either
|
||||||
|
#endif
|
||||||
injectDefaultP env path (MultP p1 p2) =
|
injectDefaultP env path (MultP p1 p2) =
|
||||||
MultP (injectDefaultP env path p1) (injectDefaultP env path p2)
|
MultP (injectDefaultP env path p1) (injectDefaultP env path p2)
|
||||||
injectDefaultP env path (AltP p1 p2) =
|
injectDefaultP env path (AltP p1 p2) =
|
||||||
AltP (injectDefaultP env path p1) (injectDefaultP env path p2)
|
AltP (injectDefaultP env path p1) (injectDefaultP env path p2)
|
||||||
injectDefaultP _env _path b@(BindP {}) = b
|
injectDefaultP _env _path b@(BindP {}) = b
|
||||||
|
|
||||||
|
|
||||||
getEnvValue :: M.Map [String] String -> [String] -> OptName -> Maybe String
|
getEnvValue :: M.Map [String] String -> [String] -> OptName -> Maybe String
|
||||||
getEnvValue env path (OptLong l) = M.lookup (path ++ [normalizeName l]) env
|
getEnvValue env path (OptLong l) = M.lookup (path ++ [normalizeName l]) env
|
||||||
getEnvValue _ _ _ = Nothing
|
getEnvValue _ _ _ = Nothing
|
||||||
|
|||||||
@ -4,7 +4,8 @@ module Scaffolding.Scaffolder (scaffold) where
|
|||||||
|
|
||||||
import Control.Arrow ((&&&))
|
import Control.Arrow ((&&&))
|
||||||
import qualified Data.ByteString.Char8 as S
|
import qualified Data.ByteString.Char8 as S
|
||||||
import Data.Conduit (runResourceT, yield, ($$), ($$+-))
|
import Data.Conduit (yield, ($$), ($$+-))
|
||||||
|
import Control.Monad.Trans.Resource (runResourceT)
|
||||||
import Data.FileEmbed (embedFile)
|
import Data.FileEmbed (embedFile)
|
||||||
import Data.String (fromString)
|
import Data.String (fromString)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
@ -14,6 +15,9 @@ import Text.ProjectTemplate (unpackTemplate, receiveFS)
|
|||||||
import System.IO
|
import System.IO
|
||||||
import Text.Shakespeare.Text (renderTextUrl, textFile)
|
import Text.Shakespeare.Text (renderTextUrl, textFile)
|
||||||
import Network.HTTP.Conduit (withManager, http, parseUrl, responseBody)
|
import Network.HTTP.Conduit (withManager, http, parseUrl, responseBody)
|
||||||
|
import Data.Maybe (isJust)
|
||||||
|
import Distribution.Text (simpleParse)
|
||||||
|
import Distribution.Package (PackageName)
|
||||||
|
|
||||||
prompt :: (String -> Maybe a) -> IO a
|
prompt :: (String -> Maybe a) -> IO a
|
||||||
prompt f = do
|
prompt f = do
|
||||||
@ -58,20 +62,15 @@ backendBS Mysql = $(embedFile "hsfiles/mysql.hsfiles")
|
|||||||
backendBS MongoDB = $(embedFile "hsfiles/mongo.hsfiles")
|
backendBS MongoDB = $(embedFile "hsfiles/mongo.hsfiles")
|
||||||
backendBS Simple = $(embedFile "hsfiles/simple.hsfiles")
|
backendBS Simple = $(embedFile "hsfiles/simple.hsfiles")
|
||||||
|
|
||||||
-- | Is the character valid for a project name?
|
validPackageName :: String -> Bool
|
||||||
validPN :: Char -> Bool
|
validPackageName s = isJust (simpleParse s :: Maybe PackageName)
|
||||||
validPN c
|
|
||||||
| 'A' <= c && c <= 'Z' = True
|
|
||||||
| 'a' <= c && c <= 'z' = True
|
|
||||||
| '0' <= c && c <= '9' = True
|
|
||||||
validPN '-' = True
|
|
||||||
validPN _ = False
|
|
||||||
|
|
||||||
scaffold :: IO ()
|
scaffold :: Bool -- ^ bare directory instead of a new subdirectory?
|
||||||
scaffold = do
|
-> IO ()
|
||||||
|
scaffold isBare = do
|
||||||
puts $ renderTextUrl undefined $(textFile "input/welcome.cg")
|
puts $ renderTextUrl undefined $(textFile "input/welcome.cg")
|
||||||
project <- prompt $ \s ->
|
project <- prompt $ \s ->
|
||||||
if all validPN s && not (null s) && s /= "test"
|
if validPackageName s && s /= "test"
|
||||||
then Just s
|
then Just s
|
||||||
else Nothing
|
else Nothing
|
||||||
let dir = project
|
let dir = project
|
||||||
@ -90,7 +89,7 @@ scaffold = do
|
|||||||
putStrLn "That's it! I'm creating your files now..."
|
putStrLn "That's it! I'm creating your files now..."
|
||||||
|
|
||||||
let sink = unpackTemplate
|
let sink = unpackTemplate
|
||||||
(receiveFS $ fromString project)
|
(receiveFS $ if isBare then "." else fromString project)
|
||||||
(T.replace "PROJECTNAME" (T.pack project))
|
(T.replace "PROJECTNAME" (T.pack project))
|
||||||
case ebackend of
|
case ebackend of
|
||||||
Left req -> withManager $ \m -> do
|
Left req -> withManager $ \m -> do
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -24,9 +24,4 @@ Take part in the community: http://yesodweb.com/page/community
|
|||||||
|
|
||||||
Start your project:
|
Start your project:
|
||||||
|
|
||||||
cd PROJECTNAME && cabal install && yesod devel
|
cd PROJECTNAME && cabal sandbox init && cabal install --enable-tests . yesod-platform yesod-bin --max-backjumps=-1 --reorder-goals && yesod devel
|
||||||
|
|
||||||
or if you use cabal-dev:
|
|
||||||
|
|
||||||
cd PROJECTNAME && cabal-dev install && yesod --dev devel
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
import Control.Monad (unless)
|
import Control.Monad (unless)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
@ -9,14 +9,19 @@ import System.Exit (ExitCode (ExitSuccess), exitWith)
|
|||||||
import System.Process (rawSystem)
|
import System.Process (rawSystem)
|
||||||
|
|
||||||
import AddHandler (addHandler)
|
import AddHandler (addHandler)
|
||||||
import Devel (DevelOpts (..), devel)
|
import Devel (DevelOpts (..), devel, DevelTermOpt(..))
|
||||||
import Keter (keter)
|
import Keter (keter)
|
||||||
import Options (injectDefaults)
|
import Options (injectDefaults)
|
||||||
import qualified Paths_yesod_bin
|
import qualified Paths_yesod_bin
|
||||||
import Scaffolding.Scaffolder
|
import Scaffolding.Scaffolder
|
||||||
|
|
||||||
|
#if MIN_VERSION_optparse_applicative(0,6,0)
|
||||||
|
import Options.Applicative.Types (ReadM (ReadM))
|
||||||
|
#else
|
||||||
import Options.Applicative.Builder.Internal (Mod, OptionFields)
|
import Options.Applicative.Builder.Internal (Mod, OptionFields)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
import HsFile (mkHsFile)
|
||||||
#ifndef WINDOWS
|
#ifndef WINDOWS
|
||||||
import Build (touch)
|
import Build (touch)
|
||||||
|
|
||||||
@ -42,7 +47,8 @@ data Options = Options
|
|||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
data Command = Init
|
data Command = Init { _initBare :: Bool }
|
||||||
|
| HsFiles
|
||||||
| Configure
|
| Configure
|
||||||
| Build { buildExtraArgs :: [String] }
|
| Build { buildExtraArgs :: [String] }
|
||||||
| Touch
|
| Touch
|
||||||
@ -56,6 +62,7 @@ data Command = Init
|
|||||||
, _develPort :: Int
|
, _develPort :: Int
|
||||||
, _proxyTimeout :: Int
|
, _proxyTimeout :: Int
|
||||||
, _noReverseProxy :: Bool
|
, _noReverseProxy :: Bool
|
||||||
|
, _interruptOnly :: Bool
|
||||||
}
|
}
|
||||||
| Test
|
| Test
|
||||||
| AddHandler
|
| AddHandler
|
||||||
@ -71,36 +78,46 @@ cabalCommand mopt
|
|||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
o <- execParser =<< injectDefaults "yesod" [ ("yesod.devel.extracabalarg" , \o args -> o { optCommand =
|
o <- execParser =<< injectDefaults "yesod"
|
||||||
case optCommand o of
|
[ ("yesod.devel.extracabalarg" , \o args -> o { optCommand =
|
||||||
d@Devel{} -> d { develExtraArgs = args }
|
case optCommand o of
|
||||||
c -> c
|
d@Devel{} -> d { develExtraArgs = args }
|
||||||
})
|
c -> c
|
||||||
, ("yesod.devel.ignore" , \o args -> o { optCommand =
|
})
|
||||||
case optCommand o of
|
, ("yesod.devel.ignore" , \o args -> o { optCommand =
|
||||||
d@Devel{} -> d { develIgnore = args }
|
case optCommand o of
|
||||||
c -> c
|
d@Devel{} -> d { develIgnore = args }
|
||||||
})
|
c -> c
|
||||||
, ("yesod.build.extracabalarg" , \o args -> o { optCommand =
|
})
|
||||||
case optCommand o of
|
, ("yesod.build.extracabalarg" , \o args -> o { optCommand =
|
||||||
b@Build{} -> b { buildExtraArgs = args }
|
case optCommand o of
|
||||||
c -> c
|
b@Build{} -> b { buildExtraArgs = args }
|
||||||
})
|
c -> c
|
||||||
] optParser'
|
})
|
||||||
let cabal xs = rawSystem' (cabalCommand o) xs
|
] optParser'
|
||||||
|
let cabal = rawSystem' (cabalCommand o)
|
||||||
case optCommand o of
|
case optCommand o of
|
||||||
Init -> scaffold
|
Init bare -> scaffold bare
|
||||||
Configure -> cabal ["configure"]
|
HsFiles -> mkHsFile
|
||||||
Build es -> touch' >> cabal ("build":es)
|
Configure -> cabal ["configure"]
|
||||||
Touch -> touch'
|
Build es -> touch' >> cabal ("build":es)
|
||||||
Devel da s f r b _ig es p t nrp -> devel (DevelOpts (optCabalPgm o == CabalDev) da (optVerbose o) r s f b p t (not nrp)) es
|
Touch -> touch'
|
||||||
Keter noRebuild -> keter (cabalCommand o) noRebuild
|
Keter noRebuild -> keter (cabalCommand o) noRebuild
|
||||||
Version -> do putStrLn ("yesod-bin version: " ++ showVersion Paths_yesod_bin.version)
|
Version -> putStrLn ("yesod-bin version: " ++ showVersion Paths_yesod_bin.version)
|
||||||
AddHandler -> addHandler
|
AddHandler -> addHandler
|
||||||
Test -> do touch'
|
Test -> cabalTest cabal
|
||||||
cabal ["configure", "--enable-tests", "-flibrary-only"]
|
Devel{..} -> devel (DevelOpts
|
||||||
cabal ["build"]
|
(optCabalPgm o == CabalDev) _develDisableApi (optVerbose o)
|
||||||
cabal ["test"]
|
_develRescan _develSuccessHook _develFailHook
|
||||||
|
_develBuildDir _develPort _proxyTimeout
|
||||||
|
(not _noReverseProxy)
|
||||||
|
(if _interruptOnly then TerminateOnlyInterrupt else TerminateOnEnter )
|
||||||
|
) develExtraArgs
|
||||||
|
where
|
||||||
|
cabalTest cabal = do touch'
|
||||||
|
_ <- cabal ["configure", "--enable-tests", "-flibrary-only"]
|
||||||
|
_ <- cabal ["build"]
|
||||||
|
cabal ["test"]
|
||||||
|
|
||||||
optParser' :: ParserInfo Options
|
optParser' :: ParserInfo Options
|
||||||
optParser' = info (helper <*> optParser) ( fullDesc <> header "Yesod Web Framework command line utility" )
|
optParser' = info (helper <*> optParser) ( fullDesc <> header "Yesod Web Framework command line utility" )
|
||||||
@ -109,8 +126,11 @@ optParser :: Parser Options
|
|||||||
optParser = Options
|
optParser = Options
|
||||||
<$> flag Cabal CabalDev ( long "dev" <> short 'd' <> help "use cabal-dev" )
|
<$> flag Cabal CabalDev ( long "dev" <> short 'd' <> help "use cabal-dev" )
|
||||||
<*> switch ( long "verbose" <> short 'v' <> help "More verbose output" )
|
<*> switch ( long "verbose" <> short 'v' <> help "More verbose output" )
|
||||||
<*> subparser ( command "init" (info (pure Init)
|
<*> subparser ( command "init"
|
||||||
|
(info (Init <$> (switch (long "bare" <> help "Create files in current folder")))
|
||||||
(progDesc "Scaffold a new site"))
|
(progDesc "Scaffold a new site"))
|
||||||
|
<> command "hsfiles" (info (pure HsFiles)
|
||||||
|
(progDesc "Create a hsfiles file for the current folder"))
|
||||||
<> command "configure" (info (pure Configure)
|
<> command "configure" (info (pure Configure)
|
||||||
(progDesc "Configure a project for building"))
|
(progDesc "Configure a project for building"))
|
||||||
<> command "build" (info (Build <$> extraCabalArgs)
|
<> command "build" (info (Build <$> extraCabalArgs)
|
||||||
@ -153,6 +173,8 @@ develOptions = Devel <$> switch ( long "disable-api" <> short 'd'
|
|||||||
<> help "Devel server timeout before returning 'not ready' message (in seconds, 0 for none)" )
|
<> help "Devel server timeout before returning 'not ready' message (in seconds, 0 for none)" )
|
||||||
<*> switch ( long "disable-reverse-proxy" <> short 'n'
|
<*> switch ( long "disable-reverse-proxy" <> short 'n'
|
||||||
<> help "Disable reverse proxy" )
|
<> help "Disable reverse proxy" )
|
||||||
|
<*> switch ( long "interrupt-only" <> short 'c'
|
||||||
|
<> help "Disable exiting when enter is pressed")
|
||||||
|
|
||||||
extraCabalArgs :: Parser [String]
|
extraCabalArgs :: Parser [String]
|
||||||
extraCabalArgs = many (strOption ( long "extra-cabal-arg" <> short 'e' <> metavar "ARG"
|
extraCabalArgs = many (strOption ( long "extra-cabal-arg" <> short 'e' <> metavar "ARG"
|
||||||
@ -164,7 +186,11 @@ optStr :: Mod OptionFields (Maybe String) -> Parser (Maybe String)
|
|||||||
optStr m =
|
optStr m =
|
||||||
nullOption $ value Nothing <> reader (success . str) <> m
|
nullOption $ value Nothing <> reader (success . str) <> m
|
||||||
where
|
where
|
||||||
|
#if MIN_VERSION_optparse_applicative(0,6,0)
|
||||||
|
success = ReadM . Right
|
||||||
|
#else
|
||||||
success = Right
|
success = Right
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Like @rawSystem@, but exits if it receives a non-success result.
|
-- | Like @rawSystem@, but exits if it receives a non-success result.
|
||||||
rawSystem' :: String -> [String] -> IO ()
|
rawSystem' :: String -> [String] -> IO ()
|
||||||
|
|||||||
@ -58,7 +58,8 @@
|
|||||||
<h1>The application isn’t built</h1>
|
<h1>The application isn’t built</h1>
|
||||||
<h2>We’ll keep trying to refresh every second</h2>
|
<h2>We’ll keep trying to refresh every second</h2>
|
||||||
<div class="msgs">
|
<div class="msgs">
|
||||||
<p>Meanwhile, here are some motivational messages:</p>
|
<script> document.getElementsByClassName("msgs")[0].style.display = "none"; </script>
|
||||||
|
<p>Meanwhile, here is a motivational message:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>You are a beautiful person making a beautiful web site.</li>
|
<li>You are a beautiful person making a beautiful web site.</li>
|
||||||
<li>Keep going, you’ve nearly fixed the bug!</li>
|
<li>Keep going, you’ve nearly fixed the bug!</li>
|
||||||
@ -66,7 +67,20 @@
|
|||||||
<li>Get a glass of water, keep hydrated.</li>
|
<li>Get a glass of water, keep hydrated.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<footer><small><script>document.write(new Date())</script></small></footer>
|
<script>
|
||||||
|
var msg = document.getElementsByClassName("msgs")[0];
|
||||||
|
var lis = Array.prototype.slice.call(msg.querySelectorAll("li"));
|
||||||
|
lis.forEach(function(li){ li.style.display = "none"; });
|
||||||
|
lis[Math.floor(Math.random() * lis.length)].style.display = "block";
|
||||||
|
msg.style.display = "block";
|
||||||
|
</script>
|
||||||
|
<footer>
|
||||||
|
<small>
|
||||||
|
<script>
|
||||||
|
document.write(new Date())
|
||||||
|
</script>
|
||||||
|
</small>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-bin
|
name: yesod-bin
|
||||||
version: 1.2.2
|
version: 1.2.11
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -34,6 +34,7 @@ executable yesod-ld-wrapper
|
|||||||
build-depends:
|
build-depends:
|
||||||
base >= 4 && < 5
|
base >= 4 && < 5
|
||||||
, Cabal
|
, Cabal
|
||||||
|
|
||||||
executable yesod-ar-wrapper
|
executable yesod-ar-wrapper
|
||||||
main-is: ghcwrapper.hs
|
main-is: ghcwrapper.hs
|
||||||
cpp-options: -DARCMD
|
cpp-options: -DARCMD
|
||||||
@ -44,15 +45,17 @@ executable yesod-ar-wrapper
|
|||||||
executable yesod
|
executable yesod
|
||||||
if os(windows)
|
if os(windows)
|
||||||
cpp-options: -DWINDOWS
|
cpp-options: -DWINDOWS
|
||||||
|
|
||||||
build-depends: base >= 4.3 && < 5
|
build-depends: base >= 4.3 && < 5
|
||||||
, ghc >= 7.0.3 && < 7.8
|
, ghc >= 7.0.3
|
||||||
, ghc-paths >= 0.1
|
, ghc-paths >= 0.1
|
||||||
, parsec >= 2.1 && < 4
|
, parsec >= 2.1 && < 4
|
||||||
, text >= 0.11
|
, text >= 0.11
|
||||||
, shakespeare-text >= 1.0 && < 1.1
|
, shakespeare
|
||||||
, shakespeare >= 1.0.2 && < 1.1
|
, shakespeare-text >= 1.0
|
||||||
, shakespeare-js >= 1.0.2 && < 1.2
|
, shakespeare >= 1.0.2 && < 2.1
|
||||||
, shakespeare-css >= 1.0.2 && < 1.1
|
, shakespeare-js >= 1.0.2
|
||||||
|
, shakespeare-css >= 1.0.2
|
||||||
, bytestring >= 0.9.1.4
|
, bytestring >= 0.9.1.4
|
||||||
, time >= 1.1.4
|
, time >= 1.1.4
|
||||||
, template-haskell
|
, template-haskell
|
||||||
@ -72,11 +75,12 @@ executable yesod
|
|||||||
, unordered-containers
|
, unordered-containers
|
||||||
, yaml >= 0.8 && < 0.9
|
, yaml >= 0.8 && < 0.9
|
||||||
, optparse-applicative >= 0.5
|
, optparse-applicative >= 0.5
|
||||||
, fsnotify >= 0.0 && < 0.1
|
, fsnotify >= 0.0 && < 0.2
|
||||||
, split >= 0.2 && < 0.3
|
, split >= 0.2 && < 0.3
|
||||||
, file-embed
|
, file-embed
|
||||||
, conduit >= 0.5 && < 1.1
|
, conduit >= 0.5 && < 1.2
|
||||||
, resourcet >= 0.3 && < 0.5
|
, conduit-extra
|
||||||
|
, resourcet >= 0.3 && < 1.2
|
||||||
, base64-bytestring
|
, base64-bytestring
|
||||||
, lifted-base
|
, lifted-base
|
||||||
, http-reverse-proxy >= 0.1.1
|
, http-reverse-proxy >= 0.1.1
|
||||||
@ -87,8 +91,11 @@ executable yesod
|
|||||||
, transformers
|
, transformers
|
||||||
, warp >= 1.3.7.5
|
, warp >= 1.3.7.5
|
||||||
, wai >= 1.4
|
, wai >= 1.4
|
||||||
|
, wai-extra
|
||||||
|
, data-default-class
|
||||||
|
, streaming-commons
|
||||||
|
|
||||||
ghc-options: -Wall -threaded
|
ghc-options: -Wall -threaded -rtsopts
|
||||||
main-is: main.hs
|
main-is: main.hs
|
||||||
other-modules: Scaffolding.Scaffolder
|
other-modules: Scaffolding.Scaffolder
|
||||||
Devel
|
Devel
|
||||||
@ -98,6 +105,7 @@ executable yesod
|
|||||||
AddHandler
|
AddHandler
|
||||||
Paths_yesod_bin
|
Paths_yesod_bin
|
||||||
Options
|
Options
|
||||||
|
HsFile
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Learn more at http://docs.yesodweb.com/
|
Learn more at http://www.yesodweb.com/
|
||||||
|
|||||||
@ -24,6 +24,8 @@ module Yesod.Core
|
|||||||
, widgetToPageContent
|
, widgetToPageContent
|
||||||
-- * Defaults
|
-- * Defaults
|
||||||
, defaultErrorHandler
|
, defaultErrorHandler
|
||||||
|
, defaultYesodMiddleware
|
||||||
|
, authorizationCheck
|
||||||
-- * Data types
|
-- * Data types
|
||||||
, AuthResult (..)
|
, AuthResult (..)
|
||||||
, unauthorizedI
|
, unauthorizedI
|
||||||
|
|||||||
@ -41,7 +41,7 @@ replaceToParent hd = hd { handlerToParent = const () }
|
|||||||
instance MonadResourceBase m => MonadHandler (HandlerT site m) where
|
instance MonadResourceBase m => MonadHandler (HandlerT site m) where
|
||||||
type HandlerSite (HandlerT site m) = site
|
type HandlerSite (HandlerT site m) = site
|
||||||
liftHandlerT (HandlerT f) = HandlerT $ liftIO . f . replaceToParent
|
liftHandlerT (HandlerT f) = HandlerT $ liftIO . f . replaceToParent
|
||||||
{-# RULES "liftHandlerT (HandlerT site IO)" forall action. liftHandlerT action = id #-}
|
{-# RULES "liftHandlerT (HandlerT site IO)" liftHandlerT = id #-}
|
||||||
|
|
||||||
instance MonadResourceBase m => MonadHandler (WidgetT site m) where
|
instance MonadResourceBase m => MonadHandler (WidgetT site m) where
|
||||||
type HandlerSite (WidgetT site m) = site
|
type HandlerSite (WidgetT site m) = site
|
||||||
@ -61,7 +61,9 @@ GOX(Monoid w, RWST r w s)
|
|||||||
GOX(Monoid w, Strict.RWST r w s)
|
GOX(Monoid w, Strict.RWST r w s)
|
||||||
GO(Strict.StateT s)
|
GO(Strict.StateT s)
|
||||||
GOX(Monoid w, Strict.WriterT w)
|
GOX(Monoid w, Strict.WriterT w)
|
||||||
|
#if !MIN_VERSION_resourcet(1,1,0)
|
||||||
GO(ExceptionT)
|
GO(ExceptionT)
|
||||||
|
#endif
|
||||||
GO(Pipe l i o u)
|
GO(Pipe l i o u)
|
||||||
GO(ConduitM i o)
|
GO(ConduitM i o)
|
||||||
#undef GO
|
#undef GO
|
||||||
@ -85,7 +87,9 @@ GOX(Monoid w, RWST r w s)
|
|||||||
GOX(Monoid w, Strict.RWST r w s)
|
GOX(Monoid w, Strict.RWST r w s)
|
||||||
GO(Strict.StateT s)
|
GO(Strict.StateT s)
|
||||||
GOX(Monoid w, Strict.WriterT w)
|
GOX(Monoid w, Strict.WriterT w)
|
||||||
|
#if !MIN_VERSION_resourcet(1,1,0)
|
||||||
GO(ExceptionT)
|
GO(ExceptionT)
|
||||||
|
#endif
|
||||||
GO(Pipe l i o u)
|
GO(Pipe l i o u)
|
||||||
GO(ConduitM i o)
|
GO(ConduitM i o)
|
||||||
#undef GO
|
#undef GO
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Yesod.Core.Class.Yesod where
|
module Yesod.Core.Class.Yesod where
|
||||||
|
|
||||||
import Control.Monad.Logger (logErrorS)
|
import Control.Monad.Logger (logErrorS)
|
||||||
@ -39,10 +40,16 @@ import Data.Default (def)
|
|||||||
import Network.Wai.Parse (lbsBackEnd,
|
import Network.Wai.Parse (lbsBackEnd,
|
||||||
tempFileBackEnd)
|
tempFileBackEnd)
|
||||||
import System.IO (stdout)
|
import System.IO (stdout)
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
import Network.Wai.Logger (ZonedDate, clockDateCacher)
|
||||||
|
import System.Log.FastLogger
|
||||||
|
import qualified GHC.IO.FD
|
||||||
|
#else
|
||||||
|
import System.Log.FastLogger.Date (ZonedDate)
|
||||||
import System.Log.FastLogger (LogStr (..), Logger,
|
import System.Log.FastLogger (LogStr (..), Logger,
|
||||||
loggerDate, loggerPutStr,
|
loggerDate, loggerPutStr,
|
||||||
mkLogger)
|
mkLogger)
|
||||||
import System.Log.FastLogger.Date (ZonedDate)
|
#endif
|
||||||
import Text.Blaze (customAttribute, textTag,
|
import Text.Blaze (customAttribute, textTag,
|
||||||
toValue, (!))
|
toValue, (!))
|
||||||
import Text.Blaze (preEscapedToMarkup)
|
import Text.Blaze (preEscapedToMarkup)
|
||||||
@ -209,7 +216,18 @@ class RenderRoute site => Yesod site where
|
|||||||
--
|
--
|
||||||
-- Default: Sends to stdout and automatically flushes on each write.
|
-- Default: Sends to stdout and automatically flushes on each write.
|
||||||
makeLogger :: site -> IO Logger
|
makeLogger :: site -> IO Logger
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
makeLogger _ = do
|
||||||
|
#if MIN_VERSION_fast_logger(2, 1, 0)
|
||||||
|
loggerSet <- newLoggerSet defaultBufSize Nothing
|
||||||
|
#else
|
||||||
|
loggerSet <- newLoggerSet defaultBufSize GHC.IO.FD.stdout
|
||||||
|
#endif
|
||||||
|
(getter, _) <- clockDateCacher
|
||||||
|
return $! Logger loggerSet getter
|
||||||
|
#else
|
||||||
makeLogger _ = mkLogger True stdout
|
makeLogger _ = mkLogger True stdout
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Send a message to the @Logger@ provided by @getLogger@.
|
-- | Send a message to the @Logger@ provided by @getLogger@.
|
||||||
--
|
--
|
||||||
@ -498,7 +516,7 @@ defaultErrorHandler (BadMethod m) = selectRep $ do
|
|||||||
<h1>Method Not Supported
|
<h1>Method Not Supported
|
||||||
<p>Method <code>#{S8.unpack m}</code> not supported
|
<p>Method <code>#{S8.unpack m}</code> not supported
|
||||||
|]
|
|]
|
||||||
provideRep $ return $ object ["message" .= ("Bad method" :: Text), "method" .= m]
|
provideRep $ return $ object ["message" .= ("Bad method" :: Text), "method" .= TE.decodeUtf8With TEE.lenientDecode m]
|
||||||
|
|
||||||
asyncHelper :: (url -> [x] -> Text)
|
asyncHelper :: (url -> [x] -> Text)
|
||||||
-> [Script (url)]
|
-> [Script (url)]
|
||||||
@ -523,6 +541,30 @@ asyncHelper render scripts jscript jsLoc =
|
|||||||
Nothing -> Nothing
|
Nothing -> Nothing
|
||||||
Just j -> Just $ jelper j
|
Just j -> Just $ jelper j
|
||||||
|
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
formatLogMessage :: IO ZonedDate
|
||||||
|
-> Loc
|
||||||
|
-> LogSource
|
||||||
|
-> LogLevel
|
||||||
|
-> LogStr -- ^ message
|
||||||
|
-> 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"
|
||||||
|
#else
|
||||||
formatLogMessage :: IO ZonedDate
|
formatLogMessage :: IO ZonedDate
|
||||||
-> Loc
|
-> Loc
|
||||||
-> LogSource
|
-> LogSource
|
||||||
@ -548,7 +590,7 @@ formatLogMessage getdate loc src level msg = do
|
|||||||
, LS $ fileLocationToString loc
|
, LS $ fileLocationToString loc
|
||||||
, LB ")\n"
|
, LB ")\n"
|
||||||
]
|
]
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Customize the cookies used by the session backend. You may
|
-- | Customize the cookies used by the session backend. You may
|
||||||
-- use this function on your definition of 'makeSessionBackend'.
|
-- use this function on your definition of 'makeSessionBackend'.
|
||||||
|
|||||||
@ -60,7 +60,8 @@ import Data.Monoid (mempty)
|
|||||||
|
|
||||||
import Text.Hamlet (Html)
|
import Text.Hamlet (Html)
|
||||||
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
|
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
|
||||||
import Data.Conduit (Source, ResourceT, Flush (Chunk), ResumableSource, mapOutput)
|
import Data.Conduit (Source, Flush (Chunk), ResumableSource, mapOutput)
|
||||||
|
import Control.Monad.Trans.Resource (ResourceT)
|
||||||
import Data.Conduit.Internal (ResumableSource (ResumableSource))
|
import Data.Conduit.Internal (ResumableSource (ResumableSource))
|
||||||
|
|
||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
@ -68,6 +69,8 @@ import Data.Aeson.Encode (fromValue)
|
|||||||
import qualified Blaze.ByteString.Builder.Char.Utf8 as Blaze
|
import qualified Blaze.ByteString.Builder.Char.Utf8 as Blaze
|
||||||
import Data.Text.Lazy.Builder (toLazyText)
|
import Data.Text.Lazy.Builder (toLazyText)
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
|
import Text.Lucius (Css, renderCss)
|
||||||
|
import Text.Julius (Javascript, unJavascript)
|
||||||
|
|
||||||
-- | Zero-length enumerator.
|
-- | Zero-length enumerator.
|
||||||
emptyContent :: Content
|
emptyContent :: Content
|
||||||
@ -107,6 +110,11 @@ instance ToContent (ContentType, Content) where
|
|||||||
instance ToContent TypedContent where
|
instance ToContent TypedContent where
|
||||||
toContent (TypedContent _ c) = c
|
toContent (TypedContent _ c) = c
|
||||||
|
|
||||||
|
instance ToContent Css where
|
||||||
|
toContent = toContent . renderCss
|
||||||
|
instance ToContent Javascript where
|
||||||
|
toContent = toContent . toLazyText . unJavascript
|
||||||
|
|
||||||
instance ToFlushBuilder builder => ToContent (Source (ResourceT IO) builder) where
|
instance ToFlushBuilder builder => ToContent (Source (ResourceT IO) builder) where
|
||||||
toContent src = ContentSource $ mapOutput toFlushBuilder src
|
toContent src = ContentSource $ mapOutput toFlushBuilder src
|
||||||
instance ToFlushBuilder builder => ToContent (ResumableSource (ResourceT IO) builder) where
|
instance ToFlushBuilder builder => ToContent (ResumableSource (ResourceT IO) builder) where
|
||||||
@ -244,6 +252,12 @@ instance HasContentType Text where
|
|||||||
instance HasContentType T.Text where
|
instance HasContentType T.Text where
|
||||||
getContentType _ = typePlain
|
getContentType _ = typePlain
|
||||||
|
|
||||||
|
instance HasContentType Css where
|
||||||
|
getContentType _ = typeCss
|
||||||
|
|
||||||
|
instance HasContentType Javascript where
|
||||||
|
getContentType _ = typeJavascript
|
||||||
|
|
||||||
-- | Any type which can be converted to 'TypedContent'.
|
-- | Any type which can be converted to 'TypedContent'.
|
||||||
--
|
--
|
||||||
-- Since 1.2.0
|
-- Since 1.2.0
|
||||||
@ -276,3 +290,8 @@ instance ToTypedContent a => ToTypedContent (DontFullyEvaluate a) where
|
|||||||
toTypedContent (DontFullyEvaluate a) =
|
toTypedContent (DontFullyEvaluate a) =
|
||||||
let TypedContent ct c = toTypedContent a
|
let TypedContent ct c = toTypedContent a
|
||||||
in TypedContent ct (ContentDontEvaluate c)
|
in TypedContent ct (ContentDontEvaluate c)
|
||||||
|
|
||||||
|
instance ToTypedContent Css where
|
||||||
|
toTypedContent = TypedContent typeCss . toContent
|
||||||
|
instance ToTypedContent Javascript where
|
||||||
|
toTypedContent = TypedContent typeJavascript . toContent
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Yesod.Core.Dispatch
|
module Yesod.Core.Dispatch
|
||||||
( -- * Quasi-quoted routing
|
( -- * Quasi-quoted routing
|
||||||
parseRoutes
|
parseRoutes
|
||||||
@ -26,6 +27,7 @@ module Yesod.Core.Dispatch
|
|||||||
, warpDebug
|
, warpDebug
|
||||||
, warpEnv
|
, warpEnv
|
||||||
, mkDefaultMiddlewares
|
, mkDefaultMiddlewares
|
||||||
|
, defaultMiddlewaresNoLogging
|
||||||
-- * WAI subsites
|
-- * WAI subsites
|
||||||
, WaiSubsite (..)
|
, WaiSubsite (..)
|
||||||
) where
|
) where
|
||||||
@ -40,7 +42,7 @@ import qualified Network.Wai as W
|
|||||||
|
|
||||||
import Data.ByteString.Lazy.Char8 ()
|
import Data.ByteString.Lazy.Char8 ()
|
||||||
|
|
||||||
import Data.Text (Text)
|
import Data.Text (Text, pack)
|
||||||
import Data.Monoid (mappend)
|
import Data.Monoid (mappend)
|
||||||
import qualified Data.ByteString as S
|
import qualified Data.ByteString as S
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
@ -63,6 +65,7 @@ import Network.Wai.Middleware.MethodOverride
|
|||||||
import qualified Network.Wai.Handler.Warp
|
import qualified Network.Wai.Handler.Warp
|
||||||
import System.Log.FastLogger
|
import System.Log.FastLogger
|
||||||
import Control.Monad.Logger
|
import Control.Monad.Logger
|
||||||
|
import Control.Monad (when)
|
||||||
import qualified Paths_yesod_core
|
import qualified Paths_yesod_core
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
|
||||||
@ -89,8 +92,13 @@ toWaiAppYre yre req =
|
|||||||
where
|
where
|
||||||
site = yreSite yre
|
site = yreSite yre
|
||||||
sendRedirect :: Yesod master => master -> [Text] -> W.Application
|
sendRedirect :: Yesod master => master -> [Text] -> W.Application
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
sendRedirect y segments' env sendResponse =
|
||||||
|
sendResponse $ W.responseLBS status301
|
||||||
|
#else
|
||||||
sendRedirect y segments' env =
|
sendRedirect y segments' env =
|
||||||
return $ W.responseLBS status301
|
return $ W.responseLBS status301
|
||||||
|
#endif
|
||||||
[ ("Content-Type", "text/plain")
|
[ ("Content-Type", "text/plain")
|
||||||
, ("Location", Blaze.ByteString.Builder.toByteString dest')
|
, ("Location", Blaze.ByteString.Builder.toByteString dest')
|
||||||
] "Redirecting"
|
] "Redirecting"
|
||||||
@ -117,6 +125,10 @@ toWaiAppYre yre req =
|
|||||||
toWaiApp :: YesodDispatch site => site -> IO W.Application
|
toWaiApp :: YesodDispatch site => site -> IO W.Application
|
||||||
toWaiApp site = do
|
toWaiApp site = do
|
||||||
logger <- makeLogger site
|
logger <- makeLogger site
|
||||||
|
toWaiAppLogger logger site
|
||||||
|
|
||||||
|
toWaiAppLogger :: YesodDispatch site => Logger -> site -> IO W.Application
|
||||||
|
toWaiAppLogger logger site = do
|
||||||
sb <- makeSessionBackend site
|
sb <- makeSessionBackend site
|
||||||
let yre = YesodRunnerEnv
|
let yre = YesodRunnerEnv
|
||||||
{ yreLogger = logger
|
{ yreLogger = logger
|
||||||
@ -143,17 +155,37 @@ toWaiApp site = do
|
|||||||
--
|
--
|
||||||
-- Since 1.2.0
|
-- Since 1.2.0
|
||||||
warp :: YesodDispatch site => Int -> site -> IO ()
|
warp :: YesodDispatch site => Int -> site -> IO ()
|
||||||
warp port site = toWaiApp site >>= Network.Wai.Handler.Warp.runSettings
|
warp port site = do
|
||||||
Network.Wai.Handler.Warp.defaultSettings
|
logger <- makeLogger site
|
||||||
{ Network.Wai.Handler.Warp.settingsPort = port
|
toWaiAppLogger logger site >>= Network.Wai.Handler.Warp.runSettings
|
||||||
, Network.Wai.Handler.Warp.settingsServerName = S8.pack $ concat
|
Network.Wai.Handler.Warp.defaultSettings
|
||||||
[ "Warp/"
|
{ Network.Wai.Handler.Warp.settingsPort = port
|
||||||
, Network.Wai.Handler.Warp.warpVersion
|
{- FIXME
|
||||||
, " + Yesod/"
|
, Network.Wai.Handler.Warp.settingsServerName = S8.pack $ concat
|
||||||
, showVersion Paths_yesod_core.version
|
[ "Warp/"
|
||||||
, " (core)"
|
, Network.Wai.Handler.Warp.warpVersion
|
||||||
]
|
, " + Yesod/"
|
||||||
}
|
, showVersion Paths_yesod_core.version
|
||||||
|
, " (core)"
|
||||||
|
]
|
||||||
|
-}
|
||||||
|
, Network.Wai.Handler.Warp.settingsOnException = const $ \e ->
|
||||||
|
when (shouldLog' e) $
|
||||||
|
messageLoggerSource
|
||||||
|
site
|
||||||
|
logger
|
||||||
|
$(qLocation >>= liftLoc)
|
||||||
|
"yesod-core"
|
||||||
|
LevelError
|
||||||
|
(toLogStr $ "Exception from Warp: " ++ show e)
|
||||||
|
}
|
||||||
|
where
|
||||||
|
shouldLog' =
|
||||||
|
#if MIN_VERSION_warp(2,1,3)
|
||||||
|
Network.Wai.Handler.Warp.defaultShouldDisplayException
|
||||||
|
#else
|
||||||
|
const True
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | A default set of middlewares.
|
-- | A default set of middlewares.
|
||||||
--
|
--
|
||||||
@ -161,14 +193,20 @@ warp port site = toWaiApp site >>= Network.Wai.Handler.Warp.runSettings
|
|||||||
mkDefaultMiddlewares :: Logger -> IO W.Middleware
|
mkDefaultMiddlewares :: Logger -> IO W.Middleware
|
||||||
mkDefaultMiddlewares logger = do
|
mkDefaultMiddlewares logger = do
|
||||||
logWare <- mkRequestLogger def
|
logWare <- mkRequestLogger def
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
{ destination = Network.Wai.Middleware.RequestLogger.Logger $ loggerSet logger
|
||||||
|
#else
|
||||||
{ destination = Logger logger
|
{ destination = Logger logger
|
||||||
|
#endif
|
||||||
, outputFormat = Apache FromSocket
|
, outputFormat = Apache FromSocket
|
||||||
}
|
}
|
||||||
return $ logWare
|
return $ logWare . defaultMiddlewaresNoLogging
|
||||||
. acceptOverride
|
|
||||||
. autohead
|
-- | All of the default middlewares, excluding logging.
|
||||||
. gzip def
|
--
|
||||||
. methodOverride
|
-- Since 1.2.12
|
||||||
|
defaultMiddlewaresNoLogging :: W.Middleware
|
||||||
|
defaultMiddlewaresNoLogging = acceptOverride . autohead . gzip def . methodOverride
|
||||||
|
|
||||||
-- | Deprecated synonym for 'warp'.
|
-- | Deprecated synonym for 'warp'.
|
||||||
warpDebug :: YesodDispatch site => Int -> site -> IO ()
|
warpDebug :: YesodDispatch site => Int -> site -> IO ()
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
@ -8,6 +9,7 @@
|
|||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
--
|
--
|
||||||
-- Module : Yesod.Handler
|
-- Module : Yesod.Handler
|
||||||
@ -73,6 +75,7 @@ module Yesod.Core.Handler
|
|||||||
, redirect
|
, redirect
|
||||||
, redirectWith
|
, redirectWith
|
||||||
, redirectToPost
|
, redirectToPost
|
||||||
|
, Fragment(..)
|
||||||
-- ** Errors
|
-- ** Errors
|
||||||
, notFound
|
, notFound
|
||||||
, badMethod
|
, badMethod
|
||||||
@ -88,6 +91,13 @@ module Yesod.Core.Handler
|
|||||||
, sendResponseStatus
|
, sendResponseStatus
|
||||||
, sendResponseCreated
|
, sendResponseCreated
|
||||||
, sendWaiResponse
|
, sendWaiResponse
|
||||||
|
, sendWaiApplication
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
, sendRawResponse
|
||||||
|
#endif
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
, sendRawResponseNoConduit
|
||||||
|
#endif
|
||||||
-- * Different representations
|
-- * Different representations
|
||||||
-- $representations
|
-- $representations
|
||||||
, selectRep
|
, selectRep
|
||||||
@ -133,6 +143,7 @@ module Yesod.Core.Handler
|
|||||||
, newIdent
|
, newIdent
|
||||||
-- * Lifting
|
-- * Lifting
|
||||||
, handlerToIO
|
, handlerToIO
|
||||||
|
, forkHandler
|
||||||
-- * i18n
|
-- * i18n
|
||||||
, getMessageRender
|
, getMessageRender
|
||||||
-- * Per-request caching
|
-- * Per-request caching
|
||||||
@ -145,18 +156,17 @@ import Yesod.Core.Internal.Request (langKey, mkFileInfoFile,
|
|||||||
mkFileInfoLBS, mkFileInfoSource)
|
mkFileInfoLBS, mkFileInfoSource)
|
||||||
|
|
||||||
import Control.Applicative ((<$>), (<|>))
|
import Control.Applicative ((<$>), (<|>))
|
||||||
import Control.Exception (evaluate)
|
import Control.Exception (evaluate, SomeException)
|
||||||
|
import Control.Exception.Lifted (handle)
|
||||||
|
|
||||||
import Control.Monad (liftM)
|
import Control.Monad (liftM, void)
|
||||||
import qualified Control.Monad.Trans.Writer as Writer
|
import qualified Control.Monad.Trans.Writer as Writer
|
||||||
|
|
||||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||||
import Control.Monad.Trans.Resource (MonadResource, liftResourceT)
|
|
||||||
|
|
||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import Control.Monad.Trans.Class (lift)
|
import Control.Monad.Trans.Class (lift)
|
||||||
import Data.Conduit (transPipe, Flush (Flush), yield, Producer)
|
|
||||||
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
|
import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
|
||||||
@ -169,10 +179,8 @@ import qualified Data.ByteString as S
|
|||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Data.Conduit (Source)
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
import Data.Maybe (mapMaybe)
|
|
||||||
import Data.Monoid (Endo (..), mappend, mempty)
|
import Data.Monoid (Endo (..), mappend, mempty)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Network.Wai.Parse as NWP
|
import qualified Network.Wai.Parse as NWP
|
||||||
@ -182,18 +190,37 @@ import Yesod.Core.Content (ToTypedContent (..), simpleConte
|
|||||||
import Yesod.Core.Internal.Util (formatRFC1123)
|
import Yesod.Core.Internal.Util (formatRFC1123)
|
||||||
import Text.Blaze.Html (preEscapedToMarkup, toHtml)
|
import Text.Blaze.Html (preEscapedToMarkup, toHtml)
|
||||||
|
|
||||||
import Control.Monad.Trans.Resource (ResourceT, runResourceT, withInternalState)
|
|
||||||
import Data.Dynamic (fromDynamic, toDyn)
|
import Data.Dynamic (fromDynamic, toDyn)
|
||||||
import qualified Data.IORef.Lifted as I
|
import qualified Data.IORef.Lifted as I
|
||||||
import Data.Maybe (listToMaybe)
|
import Data.Maybe (listToMaybe, mapMaybe)
|
||||||
import Data.Typeable (Typeable, typeOf)
|
import Data.Typeable (Typeable, typeOf)
|
||||||
|
import Web.PathPieces (PathPiece(..))
|
||||||
import Yesod.Core.Class.Handler
|
import Yesod.Core.Class.Handler
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
import Yesod.Routes.Class (Route)
|
import Yesod.Routes.Class (Route)
|
||||||
import Control.Failure (failure)
|
import Control.Exception (throwIO)
|
||||||
import Blaze.ByteString.Builder (Builder)
|
import Blaze.ByteString.Builder (Builder)
|
||||||
import Safe (headMay)
|
import Safe (headMay)
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
|
import qualified Data.Conduit.List as CL
|
||||||
|
import Control.Monad (unless)
|
||||||
|
import Control.Monad.Trans.Resource (MonadResource, InternalState, runResourceT, withInternalState, getInternalState, liftResourceT, resourceForkIO
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
#else
|
||||||
|
, ResourceT
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
import qualified System.PosixCompat.Files as PC
|
||||||
|
#endif
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
import Control.Monad.Trans.Control (control, MonadBaseControl)
|
||||||
|
#endif
|
||||||
|
import Data.Conduit (Source, transPipe, Flush (Flush), yield, Producer
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
, Sink
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
|
||||||
get :: MonadHandler m => m GHState
|
get :: MonadHandler m => m GHState
|
||||||
get = liftHandlerT $ HandlerT $ I.readIORef . handlerState
|
get = liftHandlerT $ HandlerT $ I.readIORef . handlerState
|
||||||
@ -208,7 +235,7 @@ tell :: MonadHandler m => Endo [Header] -> m ()
|
|||||||
tell hs = modify $ \g -> g { ghsHeaders = ghsHeaders g `mappend` hs }
|
tell hs = modify $ \g -> g { ghsHeaders = ghsHeaders g `mappend` hs }
|
||||||
|
|
||||||
handlerError :: MonadHandler m => HandlerContents -> m a
|
handlerError :: MonadHandler m => HandlerContents -> m a
|
||||||
handlerError = liftHandlerT . failure
|
handlerError = liftIO . throwIO
|
||||||
|
|
||||||
hcError :: MonadHandler m => ErrorResponse -> m a
|
hcError :: MonadHandler m => ErrorResponse -> m a
|
||||||
hcError = handlerError . HCError
|
hcError = handlerError . HCError
|
||||||
@ -229,21 +256,41 @@ runRequestBody = do
|
|||||||
Just rbc -> return rbc
|
Just rbc -> return rbc
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
rr <- waiRequest
|
rr <- waiRequest
|
||||||
|
#if MIN_VERSION_wai_extra(2, 0, 1)
|
||||||
|
internalState <- liftResourceT getInternalState
|
||||||
|
rbc <- liftIO $ rbHelper upload rr internalState
|
||||||
|
#elif MIN_VERSION_wai(2, 0, 0)
|
||||||
|
rbc <- liftIO $ rbHelper upload rr
|
||||||
|
#else
|
||||||
rbc <- liftResourceT $ rbHelper upload rr
|
rbc <- liftResourceT $ rbHelper upload rr
|
||||||
|
#endif
|
||||||
put x { ghsRBC = Just rbc }
|
put x { ghsRBC = Just rbc }
|
||||||
return rbc
|
return rbc
|
||||||
|
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
rbHelper :: FileUpload -> W.Request -> InternalState -> IO RequestBodyContents
|
||||||
|
rbHelper upload req internalState =
|
||||||
|
#else
|
||||||
rbHelper :: FileUpload -> W.Request -> ResourceT IO RequestBodyContents
|
rbHelper :: FileUpload -> W.Request -> ResourceT IO RequestBodyContents
|
||||||
rbHelper upload =
|
rbHelper upload req =
|
||||||
|
#endif
|
||||||
case upload of
|
case upload of
|
||||||
FileUploadMemory s -> rbHelper' s mkFileInfoLBS
|
FileUploadMemory s -> rbHelper' s mkFileInfoLBS req
|
||||||
FileUploadDisk s -> rbHelper' s mkFileInfoFile
|
#if MIN_VERSION_wai_extra(2, 0, 1)
|
||||||
FileUploadSource s -> rbHelper' s mkFileInfoSource
|
FileUploadDisk s -> rbHelper' (s internalState) mkFileInfoFile req
|
||||||
|
#else
|
||||||
|
FileUploadDisk s -> rbHelper' s mkFileInfoFile req
|
||||||
|
#endif
|
||||||
|
FileUploadSource s -> rbHelper' s mkFileInfoSource req
|
||||||
|
|
||||||
rbHelper' :: NWP.BackEnd x
|
rbHelper' :: NWP.BackEnd x
|
||||||
-> (Text -> Text -> x -> FileInfo)
|
-> (Text -> Text -> x -> FileInfo)
|
||||||
-> W.Request
|
-> W.Request
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
-> IO ([(Text, Text)], [(Text, FileInfo)])
|
||||||
|
#else
|
||||||
-> ResourceT IO ([(Text, Text)], [(Text, FileInfo)])
|
-> ResourceT IO ([(Text, Text)], [(Text, FileInfo)])
|
||||||
|
#endif
|
||||||
rbHelper' backend mkFI req =
|
rbHelper' backend mkFI req =
|
||||||
(map fix1 *** mapMaybe fix2) <$> (NWP.parseRequestBody backend req)
|
(map fix1 *** mapMaybe fix2) <$> (NWP.parseRequestBody backend req)
|
||||||
where
|
where
|
||||||
@ -327,7 +374,11 @@ handlerToIO =
|
|||||||
where
|
where
|
||||||
oldReq = handlerRequest oldHandlerData
|
oldReq = handlerRequest oldHandlerData
|
||||||
oldWaiReq = reqWaiRequest oldReq
|
oldWaiReq = reqWaiRequest oldReq
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
newWaiReq = oldWaiReq { W.requestBody = return mempty
|
||||||
|
#else
|
||||||
newWaiReq = oldWaiReq { W.requestBody = mempty
|
newWaiReq = oldWaiReq { W.requestBody = mempty
|
||||||
|
#endif
|
||||||
, W.requestBodyLength = W.KnownLength 0
|
, W.requestBodyLength = W.KnownLength 0
|
||||||
}
|
}
|
||||||
oldEnv = handlerEnv oldHandlerData
|
oldEnv = handlerEnv oldHandlerData
|
||||||
@ -358,6 +409,18 @@ handlerToIO =
|
|||||||
}
|
}
|
||||||
liftIO (f newHandlerData)
|
liftIO (f newHandlerData)
|
||||||
|
|
||||||
|
-- | forkIO for a Handler (run an action in the background)
|
||||||
|
--
|
||||||
|
-- Uses 'handlerToIO', liftResourceT, and resourceForkIO
|
||||||
|
-- for correctness and efficiency
|
||||||
|
--
|
||||||
|
-- Since 1.2.8
|
||||||
|
forkHandler :: (SomeException -> HandlerT site IO ()) -- ^ error handler
|
||||||
|
-> HandlerT site IO ()
|
||||||
|
-> HandlerT site IO ()
|
||||||
|
forkHandler onErr handler = do
|
||||||
|
yesRunner <- handlerToIO
|
||||||
|
void $ liftResourceT $ resourceForkIO $ yesRunner $ handle onErr handler
|
||||||
|
|
||||||
-- | Redirect to the given route.
|
-- | Redirect to the given route.
|
||||||
-- HTTP status code 303 for HTTP 1.1 clients and 302 for HTTP 1.0
|
-- HTTP status code 303 for HTTP 1.1 clients and 302 for HTTP 1.0
|
||||||
@ -486,8 +549,17 @@ sendFilePart :: MonadHandler m
|
|||||||
-> Integer -- ^ offset
|
-> Integer -- ^ offset
|
||||||
-> Integer -- ^ count
|
-> Integer -- ^ count
|
||||||
-> m a
|
-> m a
|
||||||
sendFilePart ct fp off count =
|
sendFilePart ct fp off count = do
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
fs <- liftIO $ PC.getFileStatus fp
|
||||||
|
handlerError $ HCSendFile ct fp $ Just W.FilePart
|
||||||
|
{ W.filePartOffset = off
|
||||||
|
, W.filePartByteCount = count
|
||||||
|
, W.filePartFileSize = fromIntegral $ PC.fileSize fs
|
||||||
|
}
|
||||||
|
#else
|
||||||
handlerError $ HCSendFile ct fp $ Just $ W.FilePart off count
|
handlerError $ HCSendFile ct fp $ Just $ W.FilePart off count
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Bypass remaining handler code and output the given content with a 200
|
-- | Bypass remaining handler code and output the given content with a 200
|
||||||
-- status code.
|
-- status code.
|
||||||
@ -514,6 +586,61 @@ sendResponseCreated url = do
|
|||||||
sendWaiResponse :: MonadHandler m => W.Response -> m b
|
sendWaiResponse :: MonadHandler m => W.Response -> m b
|
||||||
sendWaiResponse = handlerError . HCWai
|
sendWaiResponse = handlerError . HCWai
|
||||||
|
|
||||||
|
-- | Switch over to handling the current request with a WAI @Application@.
|
||||||
|
--
|
||||||
|
-- Since 1.2.17
|
||||||
|
sendWaiApplication :: MonadHandler m => W.Application -> m b
|
||||||
|
sendWaiApplication = handlerError . HCWaiApp
|
||||||
|
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
-- | Send a raw response without conduit. This is used for cases such as
|
||||||
|
-- WebSockets. Requires WAI 3.0 or later, and a web server which supports raw
|
||||||
|
-- responses (e.g., Warp).
|
||||||
|
--
|
||||||
|
-- Since 1.2.16
|
||||||
|
sendRawResponseNoConduit
|
||||||
|
:: (MonadHandler m, MonadBaseControl IO m)
|
||||||
|
=> (IO S8.ByteString -> (S8.ByteString -> IO ()) -> m ())
|
||||||
|
-> m a
|
||||||
|
sendRawResponseNoConduit raw = control $ \runInIO ->
|
||||||
|
runInIO $ sendWaiResponse $ flip W.responseRaw fallback
|
||||||
|
$ \src sink -> runInIO (raw src sink) >> return ()
|
||||||
|
where
|
||||||
|
fallback = W.responseLBS H.status500 [("Content-Type", "text/plain")]
|
||||||
|
"sendRawResponse: backend does not support raw responses"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
-- | Send a raw response. This is used for cases such as WebSockets. Requires
|
||||||
|
-- WAI 2.1 or later, and a web server which supports raw responses (e.g.,
|
||||||
|
-- Warp).
|
||||||
|
--
|
||||||
|
-- Since 1.2.7
|
||||||
|
sendRawResponse :: (MonadHandler m, MonadBaseControl IO m)
|
||||||
|
=> (Source IO S8.ByteString -> Sink S8.ByteString IO () -> m ())
|
||||||
|
-> m a
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
sendRawResponse raw = control $ \runInIO ->
|
||||||
|
runInIO $ sendWaiResponse $ flip W.responseRaw fallback
|
||||||
|
$ \src sink -> runInIO (raw (src' src) (CL.mapM_ sink)) >> return ()
|
||||||
|
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
|
||||||
|
#else
|
||||||
|
sendRawResponse raw = control $ \runInIO ->
|
||||||
|
runInIO $ sendWaiResponse $ flip W.responseRaw fallback
|
||||||
|
$ \src sink -> runInIO (raw src sink) >> return ()
|
||||||
|
where
|
||||||
|
fallback = W.responseLBS H.status500 [("Content-Type", "text/plain")]
|
||||||
|
"sendRawResponse: backend does not support raw responses"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Return a 404 not found page. Also denotes no handler available.
|
-- | Return a 404 not found page. Also denotes no handler available.
|
||||||
notFound :: MonadHandler m => m a
|
notFound :: MonadHandler m => m a
|
||||||
notFound = hcError NotFound
|
notFound = hcError NotFound
|
||||||
@ -607,7 +734,12 @@ cacheSeconds i = setHeader "Cache-Control" $ T.concat
|
|||||||
-- | Set the Expires header to some date in 2037. In other words, this content
|
-- | Set the Expires header to some date in 2037. In other words, this content
|
||||||
-- is never (realistically) expired.
|
-- is never (realistically) expired.
|
||||||
neverExpires :: MonadHandler m => m ()
|
neverExpires :: MonadHandler m => m ()
|
||||||
neverExpires = setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT"
|
neverExpires = do
|
||||||
|
setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT"
|
||||||
|
cacheSeconds oneYear
|
||||||
|
where
|
||||||
|
oneYear :: Int
|
||||||
|
oneYear = 60 * 60 * 24 * 365
|
||||||
|
|
||||||
-- | Set an Expires header in the past, meaning this content should not be
|
-- | Set an Expires header in the past, meaning this content should not be
|
||||||
-- cached.
|
-- cached.
|
||||||
@ -677,6 +809,18 @@ instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, [(key, va
|
|||||||
instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, Map.Map key val) where
|
instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, Map.Map key val) where
|
||||||
toTextUrl (url, params) = toTextUrl (url, Map.toList params)
|
toTextUrl (url, params) = toTextUrl (url, Map.toList params)
|
||||||
|
|
||||||
|
-- | Add a fragment identifier to a route to be used when
|
||||||
|
-- redirecting. For example:
|
||||||
|
--
|
||||||
|
-- > redirect (NewsfeedR :#: storyId)
|
||||||
|
--
|
||||||
|
-- Since 1.2.9.
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
-- | Lookup for session data.
|
-- | Lookup for session data.
|
||||||
lookupSession :: MonadHandler m => Text -> m (Maybe Text)
|
lookupSession :: MonadHandler m => Text -> m (Maybe Text)
|
||||||
lookupSession = (liftM . fmap) (decodeUtf8With lenientDecode) . lookupSessionBS
|
lookupSession = (liftM . fmap) (decodeUtf8With lenientDecode) . lookupSessionBS
|
||||||
@ -697,7 +841,7 @@ newIdent = do
|
|||||||
x <- get
|
x <- get
|
||||||
let i' = ghsIdent x + 1
|
let i' = ghsIdent x + 1
|
||||||
put x { ghsIdent = i' }
|
put x { ghsIdent = i' }
|
||||||
return $ T.pack $ 'h' : show i'
|
return $ T.pack $ "hident" ++ show i'
|
||||||
|
|
||||||
-- | Redirect to a POST resource.
|
-- | Redirect to a POST resource.
|
||||||
--
|
--
|
||||||
@ -916,7 +1060,7 @@ selectRep w = do
|
|||||||
]) reps
|
]) reps
|
||||||
|
|
||||||
-- match on the type for sub-type wildcards.
|
-- match on the type for sub-type wildcards.
|
||||||
-- If the accept is text/* it should match a provided text/html
|
-- If the accept is text/ * it should match a provided text/html
|
||||||
mainTypeMap = Map.fromList $ reverse $ map
|
mainTypeMap = Map.fromList $ reverse $ map
|
||||||
(\v@(ProvidedRep ct _) -> (fst $ contentTypeTypes ct, v)) reps
|
(\v@(ProvidedRep ct _) -> (fst $ contentTypeTypes ct, v)) reps
|
||||||
|
|
||||||
@ -972,7 +1116,22 @@ provideRepType ct handler =
|
|||||||
rawRequestBody :: MonadHandler m => Source m S.ByteString
|
rawRequestBody :: MonadHandler m => Source m S.ByteString
|
||||||
rawRequestBody = do
|
rawRequestBody = do
|
||||||
req <- lift waiRequest
|
req <- lift waiRequest
|
||||||
transPipe liftResourceT $ W.requestBody req
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
let loop = do
|
||||||
|
bs <- liftIO $ W.requestBody req
|
||||||
|
unless (S.null bs) $ do
|
||||||
|
yield bs
|
||||||
|
loop
|
||||||
|
loop
|
||||||
|
#else
|
||||||
|
transPipe
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
liftIO
|
||||||
|
#else
|
||||||
|
liftResourceT
|
||||||
|
#endif
|
||||||
|
(W.requestBody req)
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Stream the data from the file. Since Yesod 1.2, this has been generalized
|
-- | Stream the data from the file. Since Yesod 1.2, this has been generalized
|
||||||
-- to work in any @MonadResource@.
|
-- to work in any @MonadResource@.
|
||||||
|
|||||||
@ -40,14 +40,32 @@ import Data.Conduit.List (sourceList)
|
|||||||
import Data.Conduit.Binary (sourceFile, sinkFile)
|
import Data.Conduit.Binary (sourceFile, sinkFile)
|
||||||
import Data.Word (Word64)
|
import Data.Word (Word64)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import Control.Monad.Trans.Resource (runResourceT, ResourceT)
|
||||||
import Control.Exception (throwIO)
|
import Control.Exception (throwIO)
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import Data.IORef
|
||||||
|
|
||||||
-- | Impose a limit on the size of the request body.
|
-- | Impose a limit on the size of the request body.
|
||||||
limitRequestBody :: Word64 -> W.Request -> W.Request
|
limitRequestBody :: Word64 -> W.Request -> IO W.Request
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
limitRequestBody maxLen req = do
|
||||||
|
ref <- newIORef maxLen
|
||||||
|
return req
|
||||||
|
{ W.requestBody = do
|
||||||
|
bs <- W.requestBody req
|
||||||
|
remaining <- readIORef ref
|
||||||
|
let len = fromIntegral $ S8.length bs
|
||||||
|
remaining' = remaining - len
|
||||||
|
if remaining < len
|
||||||
|
then throwIO $ HCWai tooLargeResponse
|
||||||
|
else do
|
||||||
|
writeIORef ref remaining'
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
#else
|
||||||
limitRequestBody maxLen req =
|
limitRequestBody maxLen req =
|
||||||
req { W.requestBody = W.requestBody req $= limit maxLen }
|
return req { W.requestBody = W.requestBody req $= limit maxLen }
|
||||||
where
|
where
|
||||||
tooLarge = liftIO $ throwIO $ HCWai tooLargeResponse
|
tooLarge = liftIO $ throwIO $ HCWai tooLargeResponse
|
||||||
|
|
||||||
@ -62,6 +80,7 @@ limitRequestBody maxLen req =
|
|||||||
else do
|
else do
|
||||||
yield bs
|
yield bs
|
||||||
limit $ remaining - len
|
limit $ remaining - len
|
||||||
|
#endif
|
||||||
|
|
||||||
tooLargeResponse :: W.Response
|
tooLargeResponse :: W.Response
|
||||||
tooLargeResponse = W.responseLBS
|
tooLargeResponse = W.responseLBS
|
||||||
@ -74,7 +93,7 @@ parseWaiRequest :: RandomGen g
|
|||||||
-> SessionMap
|
-> SessionMap
|
||||||
-> Bool
|
-> Bool
|
||||||
-> Maybe Word64 -- ^ max body size
|
-> Maybe Word64 -- ^ max body size
|
||||||
-> (Either YesodRequest (g -> YesodRequest))
|
-> (Either (IO YesodRequest) (g -> IO YesodRequest))
|
||||||
parseWaiRequest env session useToken mmaxBodySize =
|
parseWaiRequest env session useToken mmaxBodySize =
|
||||||
-- In most cases, we won't need to generate any random values. Therefore,
|
-- In most cases, we won't need to generate any random values. Therefore,
|
||||||
-- we split our results: if we need a random generator, return a Right
|
-- we split our results: if we need a random generator, return a Right
|
||||||
@ -84,17 +103,19 @@ parseWaiRequest env session useToken mmaxBodySize =
|
|||||||
Left token -> Left $ mkRequest token
|
Left token -> Left $ mkRequest token
|
||||||
Right mkToken -> Right $ mkRequest . mkToken
|
Right mkToken -> Right $ mkRequest . mkToken
|
||||||
where
|
where
|
||||||
mkRequest token' = YesodRequest
|
mkRequest token' = do
|
||||||
{ reqGetParams = gets
|
envLimited <- maybe return limitRequestBody mmaxBodySize env
|
||||||
, reqCookies = cookies
|
return YesodRequest
|
||||||
, reqWaiRequest = maybe id limitRequestBody mmaxBodySize env
|
{ reqGetParams = gets
|
||||||
, reqLangs = langs''
|
, reqCookies = cookies
|
||||||
, reqToken = token'
|
, reqWaiRequest = envLimited
|
||||||
, reqSession = if useToken
|
, reqLangs = langs''
|
||||||
then Map.delete tokenKey session
|
, reqToken = token'
|
||||||
else session
|
, reqSession = if useToken
|
||||||
, reqAccept = httpAccept env
|
then Map.delete tokenKey session
|
||||||
}
|
else session
|
||||||
|
, reqAccept = httpAccept env
|
||||||
|
}
|
||||||
gets = textQueryString env
|
gets = textQueryString env
|
||||||
reqCookie = lookup "Cookie" $ W.requestHeaders env
|
reqCookie = lookup "Cookie" $ W.requestHeaders env
|
||||||
cookies = maybe [] parseCookiesText reqCookie
|
cookies = maybe [] parseCookiesText reqCookie
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PatternGuards #-}
|
{-# LANGUAGE PatternGuards #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
@ -12,6 +13,13 @@ import qualified Data.ByteString.Char8 as S8
|
|||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
import Data.Conduit (transPipe)
|
||||||
|
import Control.Monad.Trans.Resource (runInternalState, getInternalState, runResourceT, InternalState, closeInternalState)
|
||||||
|
import Control.Monad.Trans.Class (lift)
|
||||||
|
import Network.Wai.Internal
|
||||||
|
import Control.Exception (finally)
|
||||||
|
#endif
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import Web.Cookie (renderSetCookie)
|
import Web.Cookie (renderSetCookie)
|
||||||
import Yesod.Core.Content
|
import Yesod.Core.Content
|
||||||
@ -25,14 +33,20 @@ import qualified Data.ByteString.Lazy as L
|
|||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Yesod.Core.Internal.Request (tokenKey)
|
import Yesod.Core.Internal.Request (tokenKey)
|
||||||
import Data.Text.Encoding (encodeUtf8)
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
|
import Data.Conduit (Flush (..), ($$))
|
||||||
|
import qualified Data.Conduit.List as CL
|
||||||
|
|
||||||
yarToResponse :: Monad m
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
=> YesodResponse
|
yarToResponse :: YesodResponse
|
||||||
-> (SessionMap -> m [Header]) -- ^ save session
|
-> (SessionMap -> IO [Header]) -- ^ save session
|
||||||
-> YesodRequest
|
-> YesodRequest
|
||||||
-> m Response
|
-> Request
|
||||||
yarToResponse (YRWai a) _ _ = return a
|
-> InternalState
|
||||||
yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq = do
|
-> (Response -> IO ResponseReceived)
|
||||||
|
-> IO ResponseReceived
|
||||||
|
yarToResponse (YRWai a) _ _ _ _ sendResponse = sendResponse a
|
||||||
|
yarToResponse (YRWaiApp app) _ _ req _ sendResponse = app req sendResponse
|
||||||
|
yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq req is sendResponse = do
|
||||||
extraHeaders <- do
|
extraHeaders <- do
|
||||||
let nsToken = maybe
|
let nsToken = maybe
|
||||||
newSess
|
newSess
|
||||||
@ -43,6 +57,88 @@ yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq = do
|
|||||||
let finalHeaders = extraHeaders ++ map headerToPair hs
|
let finalHeaders = extraHeaders ++ map headerToPair hs
|
||||||
finalHeaders' len = ("Content-Length", S8.pack $ show len)
|
finalHeaders' len = ("Content-Length", S8.pack $ show len)
|
||||||
: finalHeaders
|
: finalHeaders
|
||||||
|
|
||||||
|
let go (ContentBuilder b mlen) = do
|
||||||
|
let hs' = maybe finalHeaders finalHeaders' mlen
|
||||||
|
sendResponse $ ResponseBuilder s hs' b
|
||||||
|
go (ContentFile fp p) = do
|
||||||
|
sendResponse $ ResponseFile s finalHeaders fp p
|
||||||
|
go (ContentSource body) = sendResponse $ responseStream s finalHeaders
|
||||||
|
$ \sendChunk flush -> do
|
||||||
|
transPipe (flip runInternalState is) body
|
||||||
|
$$ CL.mapM_ (\mchunk ->
|
||||||
|
case mchunk of
|
||||||
|
Flush -> flush
|
||||||
|
Chunk builder -> sendChunk builder)
|
||||||
|
go (ContentDontEvaluate c') = go c'
|
||||||
|
go c
|
||||||
|
where
|
||||||
|
s
|
||||||
|
| s' == defaultStatus = H.status200
|
||||||
|
| otherwise = s'
|
||||||
|
|
||||||
|
#else
|
||||||
|
yarToResponse :: YesodResponse
|
||||||
|
-> (SessionMap -> IO [Header]) -- ^ save session
|
||||||
|
-> YesodRequest
|
||||||
|
-> Request
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
-> InternalState
|
||||||
|
#endif
|
||||||
|
-> IO Response
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
yarToResponse (YRWaiApp app) _ _ req _ = app req
|
||||||
|
yarToResponse (YRWai a) _ _ _ is =
|
||||||
|
case a of
|
||||||
|
ResponseSource s hs w -> return $ ResponseSource s hs $ \f ->
|
||||||
|
w f `finally` closeInternalState is
|
||||||
|
ResponseBuilder{} -> do
|
||||||
|
closeInternalState is
|
||||||
|
return a
|
||||||
|
ResponseFile{} -> do
|
||||||
|
closeInternalState is
|
||||||
|
return a
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
-- Ignore the fallback provided, in case it refers to a ResourceT state
|
||||||
|
-- in a ResponseSource.
|
||||||
|
ResponseRaw raw _ -> return $ ResponseRaw
|
||||||
|
(\f -> raw f `finally` closeInternalState is)
|
||||||
|
(responseLBS H.status500 [("Content-Type", "text/plain")]
|
||||||
|
"yarToResponse: backend does not support raw responses")
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
yarToResponse (YRWai a) _ _ _ = return a
|
||||||
|
#endif
|
||||||
|
yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq req
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
is
|
||||||
|
#endif
|
||||||
|
= do
|
||||||
|
extraHeaders <- do
|
||||||
|
let nsToken = maybe
|
||||||
|
newSess
|
||||||
|
(\n -> Map.insert tokenKey (encodeUtf8 n) newSess)
|
||||||
|
(reqToken yreq)
|
||||||
|
sessionHeaders <- saveSession nsToken
|
||||||
|
return $ ("Content-Type", ct) : map headerToPair sessionHeaders
|
||||||
|
let finalHeaders = extraHeaders ++ map headerToPair hs
|
||||||
|
finalHeaders' len = ("Content-Length", S8.pack $ show len)
|
||||||
|
: finalHeaders
|
||||||
|
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
let go (ContentBuilder b mlen) = do
|
||||||
|
let hs' = maybe finalHeaders finalHeaders' mlen
|
||||||
|
closeInternalState is
|
||||||
|
return $ ResponseBuilder s hs' b
|
||||||
|
go (ContentFile fp p) = do
|
||||||
|
closeInternalState is
|
||||||
|
return $ ResponseFile s finalHeaders fp p
|
||||||
|
go (ContentSource body) = return $ ResponseSource s finalHeaders $ \f ->
|
||||||
|
f (transPipe (flip runInternalState is) body) `finally`
|
||||||
|
closeInternalState is
|
||||||
|
go (ContentDontEvaluate c') = go c'
|
||||||
|
go c
|
||||||
|
#else
|
||||||
let go (ContentBuilder b mlen) =
|
let go (ContentBuilder b mlen) =
|
||||||
let hs' = maybe finalHeaders finalHeaders' mlen
|
let hs' = maybe finalHeaders finalHeaders' mlen
|
||||||
in ResponseBuilder s hs' b
|
in ResponseBuilder s hs' b
|
||||||
@ -50,10 +146,12 @@ yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq = do
|
|||||||
go (ContentSource body) = ResponseSource s finalHeaders body
|
go (ContentSource body) = ResponseSource s finalHeaders body
|
||||||
go (ContentDontEvaluate c') = go c'
|
go (ContentDontEvaluate c') = go c'
|
||||||
return $ go c
|
return $ go c
|
||||||
|
#endif
|
||||||
where
|
where
|
||||||
s
|
s
|
||||||
| s' == defaultStatus = H.status200
|
| s' == defaultStatus = H.status200
|
||||||
| otherwise = s'
|
| otherwise = s'
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Indicates that the user provided no specific status code to be used, and
|
-- | Indicates that the user provided no specific status code to be used, and
|
||||||
-- therefore the default status code should be used. For normal responses, this
|
-- therefore the default status code should be used. For normal responses, this
|
||||||
@ -87,7 +185,9 @@ headerToPair (Header key value) = (CI.mk key, value)
|
|||||||
evaluateContent :: Content -> IO (Either ErrorResponse Content)
|
evaluateContent :: Content -> IO (Either ErrorResponse Content)
|
||||||
evaluateContent (ContentBuilder b mlen) = handle f $ do
|
evaluateContent (ContentBuilder b mlen) = handle f $ do
|
||||||
let lbs = toLazyByteString b
|
let lbs = toLazyByteString b
|
||||||
L.length lbs `seq` return (Right $ ContentBuilder (fromLazyByteString lbs) mlen)
|
len = L.length lbs
|
||||||
|
mlen' = maybe (Just $ fromIntegral len) Just mlen
|
||||||
|
len `seq` return (Right $ ContentBuilder (fromLazyByteString lbs) mlen')
|
||||||
where
|
where
|
||||||
f :: SomeException -> IO (Either ErrorResponse Content)
|
f :: SomeException -> IO (Either ErrorResponse Content)
|
||||||
f = return . Left . InternalError . T.pack . show
|
f = return . Left . InternalError . T.pack . show
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PatternGuards #-}
|
{-# LANGUAGE PatternGuards #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
@ -9,13 +10,15 @@ module Yesod.Core.Internal.Run where
|
|||||||
import Yesod.Core.Internal.Response
|
import Yesod.Core.Internal.Response
|
||||||
import Blaze.ByteString.Builder (toByteString)
|
import Blaze.ByteString.Builder (toByteString)
|
||||||
import Control.Applicative ((<$>))
|
import Control.Applicative ((<$>))
|
||||||
import Control.Exception (fromException)
|
import Control.Exception (fromException, bracketOnError, evaluate)
|
||||||
|
import qualified Control.Exception as E
|
||||||
import Control.Exception.Lifted (catch)
|
import Control.Exception.Lifted (catch)
|
||||||
|
import Control.Monad (mplus)
|
||||||
import Control.Monad.IO.Class (MonadIO)
|
import Control.Monad.IO.Class (MonadIO)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Logger (LogLevel (LevelError), LogSource,
|
import Control.Monad.Logger (LogLevel (LevelError), LogSource,
|
||||||
liftLoc)
|
liftLoc)
|
||||||
import Control.Monad.Trans.Resource (runResourceT, withInternalState, runInternalState)
|
import Control.Monad.Trans.Resource (runResourceT, withInternalState, runInternalState, createInternalState, closeInternalState)
|
||||||
import qualified Data.ByteString as S
|
import qualified Data.ByteString as S
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
import qualified Data.IORef as I
|
import qualified Data.IORef as I
|
||||||
@ -31,8 +34,13 @@ import Data.Text.Encoding.Error (lenientDecode)
|
|||||||
import Language.Haskell.TH.Syntax (Loc, qLocation)
|
import Language.Haskell.TH.Syntax (Loc, qLocation)
|
||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
import Network.Wai.Internal
|
||||||
|
#endif
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
|
#if !MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
import System.Log.FastLogger (Logger)
|
import System.Log.FastLogger (Logger)
|
||||||
|
#endif
|
||||||
import System.Log.FastLogger (LogStr, toLogStr)
|
import System.Log.FastLogger (LogStr, toLogStr)
|
||||||
import System.Random (newStdGen)
|
import System.Random (newStdGen)
|
||||||
import Yesod.Core.Content
|
import Yesod.Core.Content
|
||||||
@ -41,6 +49,19 @@ import Yesod.Core.Types
|
|||||||
import Yesod.Core.Internal.Request (parseWaiRequest,
|
import Yesod.Core.Internal.Request (parseWaiRequest,
|
||||||
tooLargeResponse)
|
tooLargeResponse)
|
||||||
import Yesod.Routes.Class (Route, renderRoute)
|
import Yesod.Routes.Class (Route, renderRoute)
|
||||||
|
import Control.DeepSeq (($!!), NFData)
|
||||||
|
import Control.Monad (liftM)
|
||||||
|
|
||||||
|
returnDeepSessionMap :: Monad m => SessionMap -> m SessionMap
|
||||||
|
#if MIN_VERSION_bytestring(0, 10, 0)
|
||||||
|
returnDeepSessionMap sm = return $!! sm
|
||||||
|
#else
|
||||||
|
returnDeepSessionMap sm = fmap unWrappedBS `liftM` (return $!! fmap WrappedBS sm)
|
||||||
|
|
||||||
|
-- | Work around missing NFData instance for bytestring 0.9.
|
||||||
|
newtype WrappedBS = WrappedBS { unWrappedBS :: S8.ByteString }
|
||||||
|
instance NFData WrappedBS
|
||||||
|
#endif
|
||||||
|
|
||||||
-- | Function used internally by Yesod in the process of converting a
|
-- | Function used internally by Yesod in the process of converting a
|
||||||
-- 'HandlerT' into an 'Application'. Should not be needed by users.
|
-- 'HandlerT' into an 'Application'. Should not be needed by users.
|
||||||
@ -71,29 +92,43 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
|
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
|
||||||
$ fromException e)
|
$ fromException e)
|
||||||
state <- liftIO $ I.readIORef istate
|
state <- liftIO $ I.readIORef istate
|
||||||
let finalSession = ghsSession state
|
|
||||||
let headers = ghsHeaders state
|
(finalSession, mcontents1) <- (do
|
||||||
let contents = either id (HCContent defaultStatus . toTypedContent) contents'
|
finalSession <- returnDeepSessionMap (ghsSession state)
|
||||||
|
return (finalSession, Nothing)) `E.catch` \e -> return
|
||||||
|
(Map.empty, Just $! HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
||||||
|
|
||||||
|
(headers, mcontents2) <- (do
|
||||||
|
headers <- return $!! appEndo (ghsHeaders state) []
|
||||||
|
return (headers, Nothing)) `E.catch` \e -> return
|
||||||
|
([], Just $! HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
||||||
|
|
||||||
|
let contents =
|
||||||
|
case mcontents1 `mplus` mcontents2 of
|
||||||
|
Just x -> x
|
||||||
|
Nothing -> either id (HCContent defaultStatus . toTypedContent) contents'
|
||||||
let handleError e = flip runInternalState resState $ do
|
let handleError e = flip runInternalState resState $ do
|
||||||
yar <- rheOnError e yreq
|
yar <- rheOnError e yreq
|
||||||
{ reqSession = finalSession
|
{ reqSession = finalSession
|
||||||
}
|
}
|
||||||
case yar of
|
case yar of
|
||||||
YRPlain status' hs ct c sess ->
|
YRPlain status' hs ct c sess ->
|
||||||
let hs' = appEndo headers hs
|
let hs' = headers ++ hs
|
||||||
status
|
status
|
||||||
| status' == defaultStatus = getStatus e
|
| status' == defaultStatus = getStatus e
|
||||||
| otherwise = status'
|
| otherwise = status'
|
||||||
in return $ YRPlain status hs' ct c sess
|
in return $ YRPlain status hs' ct c sess
|
||||||
YRWai _ -> return yar
|
YRWai _ -> return yar
|
||||||
let sendFile' ct fp p =
|
let sendFile' ct fp p =
|
||||||
return $ YRPlain H.status200 (appEndo headers []) ct (ContentFile fp p) finalSession
|
return $ YRPlain H.status200 headers ct (ContentFile fp p) finalSession
|
||||||
case contents of
|
contents1 <- evaluate contents `E.catch` \e -> return
|
||||||
|
(HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
||||||
|
case contents1 of
|
||||||
HCContent status (TypedContent ct c) -> do
|
HCContent status (TypedContent ct c) -> do
|
||||||
ec' <- liftIO $ evaluateContent c
|
ec' <- liftIO $ evaluateContent c
|
||||||
case ec' of
|
case ec' of
|
||||||
Left e -> handleError e
|
Left e -> handleError e
|
||||||
Right c' -> return $ YRPlain status (appEndo headers []) ct c' finalSession
|
Right c' -> return $ YRPlain status headers ct c' finalSession
|
||||||
HCError e -> handleError e
|
HCError e -> handleError e
|
||||||
HCRedirect status loc -> do
|
HCRedirect status loc -> do
|
||||||
let disable_caching x =
|
let disable_caching x =
|
||||||
@ -101,7 +136,7 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
: Header "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
|
: Header "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
|
||||||
: x
|
: x
|
||||||
hs = (if status /= H.movedPermanently301 then disable_caching else id)
|
hs = (if status /= H.movedPermanently301 then disable_caching else id)
|
||||||
$ Header "Location" (encodeUtf8 loc) : appEndo headers []
|
$ Header "Location" (encodeUtf8 loc) : headers
|
||||||
return $ YRPlain
|
return $ YRPlain
|
||||||
status hs typePlain emptyContent
|
status hs typePlain emptyContent
|
||||||
finalSession
|
finalSession
|
||||||
@ -109,7 +144,7 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
(sendFile' ct fp p)
|
(sendFile' ct fp p)
|
||||||
(handleError . toErrorHandler)
|
(handleError . toErrorHandler)
|
||||||
HCCreated loc -> do
|
HCCreated loc -> do
|
||||||
let hs = Header "Location" (encodeUtf8 loc) : appEndo headers []
|
let hs = Header "Location" (encodeUtf8 loc) : headers
|
||||||
return $ YRPlain
|
return $ YRPlain
|
||||||
H.status201
|
H.status201
|
||||||
hs
|
hs
|
||||||
@ -117,6 +152,7 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
emptyContent
|
emptyContent
|
||||||
finalSession
|
finalSession
|
||||||
HCWai r -> return $ YRWai r
|
HCWai r -> return $ YRWai r
|
||||||
|
HCWaiApp a -> return $ YRWaiApp a
|
||||||
|
|
||||||
safeEh :: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
|
safeEh :: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
|
||||||
-> ErrorResponse
|
-> ErrorResponse
|
||||||
@ -179,20 +215,27 @@ runFakeHandler fakeSessionMap logger site handler = liftIO $ do
|
|||||||
typePlain
|
typePlain
|
||||||
(toContent ("runFakeHandler: errHandler" :: S8.ByteString))
|
(toContent ("runFakeHandler: errHandler" :: S8.ByteString))
|
||||||
(reqSession req)
|
(reqSession req)
|
||||||
fakeWaiRequest =
|
fakeWaiRequest = Request
|
||||||
Request
|
|
||||||
{ requestMethod = "POST"
|
{ requestMethod = "POST"
|
||||||
, httpVersion = H.http11
|
, httpVersion = H.http11
|
||||||
, rawPathInfo = "/runFakeHandler/pathInfo"
|
, rawPathInfo = "/runFakeHandler/pathInfo"
|
||||||
, rawQueryString = ""
|
, rawQueryString = ""
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
, requestHeaderHost = Nothing
|
||||||
|
#else
|
||||||
, serverName = "runFakeHandler-serverName"
|
, serverName = "runFakeHandler-serverName"
|
||||||
, serverPort = 80
|
, serverPort = 80
|
||||||
|
#endif
|
||||||
, requestHeaders = []
|
, requestHeaders = []
|
||||||
, isSecure = False
|
, isSecure = False
|
||||||
, remoteHost = error "runFakeHandler-remoteHost"
|
, remoteHost = error "runFakeHandler-remoteHost"
|
||||||
, pathInfo = ["runFakeHandler", "pathInfo"]
|
, pathInfo = ["runFakeHandler", "pathInfo"]
|
||||||
, queryString = []
|
, queryString = []
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
, requestBody = return mempty
|
||||||
|
#else
|
||||||
, requestBody = mempty
|
, requestBody = mempty
|
||||||
|
#endif
|
||||||
, vault = mempty
|
, vault = mempty
|
||||||
, requestBodyLength = KnownLength 0
|
, requestBodyLength = KnownLength 0
|
||||||
}
|
}
|
||||||
@ -215,8 +258,13 @@ yesodRunner :: (ToTypedContent res, Yesod site)
|
|||||||
-> YesodRunnerEnv site
|
-> YesodRunnerEnv site
|
||||||
-> Maybe (Route site)
|
-> Maybe (Route site)
|
||||||
-> Application
|
-> Application
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
yesodRunner handler' YesodRunnerEnv {..} route req sendResponse
|
||||||
|
| Just maxLen <- mmaxLen, KnownLength len <- requestBodyLength req, maxLen < len = sendResponse tooLargeResponse
|
||||||
|
#else
|
||||||
yesodRunner handler' YesodRunnerEnv {..} route req
|
yesodRunner handler' YesodRunnerEnv {..} route req
|
||||||
| Just maxLen <- mmaxLen, KnownLength len <- requestBodyLength req, maxLen < len = return tooLargeResponse
|
| Just maxLen <- mmaxLen, KnownLength len <- requestBodyLength req, maxLen < len = return tooLargeResponse
|
||||||
|
#endif
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
let dontSaveSession _ = return []
|
let dontSaveSession _ = return []
|
||||||
(session, saveSession) <- liftIO $ do
|
(session, saveSession) <- liftIO $ do
|
||||||
@ -243,8 +291,25 @@ yesodRunner handler' YesodRunnerEnv {..} route req
|
|||||||
rhe = rheSafe
|
rhe = rheSafe
|
||||||
{ rheOnError = runHandler rheSafe . errorHandler
|
{ rheOnError = runHandler rheSafe . errorHandler
|
||||||
}
|
}
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
|
||||||
|
E.bracket createInternalState closeInternalState $ \is -> do
|
||||||
|
yreq' <- yreq
|
||||||
|
yar <- runInternalState (runHandler rhe handler yreq') is
|
||||||
|
yarToResponse yar saveSession yreq' req is sendResponse
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#if MIN_VERSION_wai(2, 0, 0)
|
||||||
|
bracketOnError createInternalState closeInternalState $ \is -> do
|
||||||
|
yreq' <- yreq
|
||||||
|
yar <- runInternalState (runHandler rhe handler yreq') is
|
||||||
|
liftIO $ yarToResponse yar saveSession yreq' req is
|
||||||
|
#else
|
||||||
yar <- runHandler rhe handler yreq
|
yar <- runHandler rhe handler yreq
|
||||||
liftIO $ yarToResponse yar saveSession yreq
|
liftIO $ yarToResponse yar saveSession yreq req
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
where
|
where
|
||||||
mmaxLen = maximumContentLength yreSite route
|
mmaxLen = maximumContentLength yreSite route
|
||||||
handler = yesodMiddleware handler'
|
handler = yesodMiddleware handler'
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE TypeSynonymInstances, OverloadedStrings #-}
|
{-# LANGUAGE TypeSynonymInstances, OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
module Yesod.Core.Json
|
module Yesod.Core.Json
|
||||||
( -- * Convert from a JSON value
|
( -- * Convert from a JSON value
|
||||||
@ -10,6 +11,7 @@ module Yesod.Core.Json
|
|||||||
-- * Convert to a JSON value
|
-- * Convert to a JSON value
|
||||||
, parseJsonBody
|
, parseJsonBody
|
||||||
, parseJsonBody_
|
, parseJsonBody_
|
||||||
|
, requireJsonBody
|
||||||
|
|
||||||
-- * Produce JSON values
|
-- * Produce JSON values
|
||||||
, J.Value (..)
|
, J.Value (..)
|
||||||
@ -27,6 +29,7 @@ module Yesod.Core.Json
|
|||||||
|
|
||||||
import Yesod.Core.Handler (HandlerT, getRequest, invalidArgs, redirect, selectRep, provideRep, rawRequestBody, ProvidedRep)
|
import Yesod.Core.Handler (HandlerT, getRequest, invalidArgs, redirect, selectRep, provideRep, rawRequestBody, ProvidedRep)
|
||||||
import Control.Monad.Trans.Writer (Writer)
|
import Control.Monad.Trans.Writer (Writer)
|
||||||
|
import Control.Monad.Trans.Resource (runExceptionT)
|
||||||
import Data.Monoid (Endo)
|
import Data.Monoid (Endo)
|
||||||
import Yesod.Core.Content (TypedContent)
|
import Yesod.Core.Content (TypedContent)
|
||||||
import Yesod.Core.Types (reqAccept)
|
import Yesod.Core.Types (reqAccept)
|
||||||
@ -41,6 +44,7 @@ import Data.Conduit.Attoparsec (sinkParser)
|
|||||||
import Data.Text (pack)
|
import Data.Text (pack)
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import Data.Conduit
|
import Data.Conduit
|
||||||
|
import Data.Conduit.Lift
|
||||||
import qualified Data.ByteString.Char8 as B8
|
import qualified Data.ByteString.Char8 as B8
|
||||||
import Data.Maybe (listToMaybe)
|
import Data.Maybe (listToMaybe)
|
||||||
import Control.Monad (liftM)
|
import Control.Monad (liftM)
|
||||||
@ -84,10 +88,18 @@ provideJson = provideRep . return . J.toJSON
|
|||||||
-- If you want the raw JSON value, just ask for a @'J.Result'
|
-- If you want the raw JSON value, just ask for a @'J.Result'
|
||||||
-- 'J.Value'@.
|
-- 'J.Value'@.
|
||||||
--
|
--
|
||||||
|
-- Note that this function will consume the request body. As such, calling it
|
||||||
|
-- twice will result in a parse error on the second call, since the request
|
||||||
|
-- body will no longer be available.
|
||||||
|
--
|
||||||
-- /Since: 0.3.0/
|
-- /Since: 0.3.0/
|
||||||
parseJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a)
|
parseJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a)
|
||||||
parseJsonBody = do
|
parseJsonBody = do
|
||||||
|
#if MIN_VERSION_resourcet(1,1,0)
|
||||||
|
eValue <- rawRequestBody $$ runCatchC (sinkParser JP.value')
|
||||||
|
#else
|
||||||
eValue <- runExceptionT $ rawRequestBody $$ sinkParser JP.value'
|
eValue <- runExceptionT $ rawRequestBody $$ sinkParser JP.value'
|
||||||
|
#endif
|
||||||
return $ case eValue of
|
return $ case eValue of
|
||||||
Left e -> J.Error $ show e
|
Left e -> J.Error $ show e
|
||||||
Right value -> J.fromJSON value
|
Right value -> J.fromJSON value
|
||||||
@ -95,7 +107,13 @@ parseJsonBody = do
|
|||||||
-- | Same as 'parseJsonBody', but return an invalid args response on a parse
|
-- | Same as 'parseJsonBody', but return an invalid args response on a parse
|
||||||
-- error.
|
-- error.
|
||||||
parseJsonBody_ :: (MonadHandler m, J.FromJSON a) => m a
|
parseJsonBody_ :: (MonadHandler m, J.FromJSON a) => m a
|
||||||
parseJsonBody_ = do
|
parseJsonBody_ = requireJsonBody
|
||||||
|
{-# DEPRECATED parseJsonBody_ "Use requireJsonBody instead" #-}
|
||||||
|
|
||||||
|
-- | Same as 'parseJsonBody', but return an invalid args response on a parse
|
||||||
|
-- error.
|
||||||
|
requireJsonBody :: (MonadHandler m, J.FromJSON a) => m a
|
||||||
|
requireJsonBody = do
|
||||||
ra <- parseJsonBody
|
ra <- parseJsonBody
|
||||||
case ra of
|
case ra of
|
||||||
J.Error s -> invalidArgs [pack s]
|
J.Error s -> invalidArgs [pack s]
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Yesod.Core.Types where
|
module Yesod.Core.Types where
|
||||||
|
|
||||||
import qualified Blaze.ByteString.Builder as BBuilder
|
import qualified Blaze.ByteString.Builder as BBuilder
|
||||||
@ -15,16 +16,21 @@ import Control.Arrow (first)
|
|||||||
import Control.Exception (Exception)
|
import Control.Exception (Exception)
|
||||||
import Control.Monad (liftM, ap)
|
import Control.Monad (liftM, ap)
|
||||||
import Control.Monad.Base (MonadBase (liftBase))
|
import Control.Monad.Base (MonadBase (liftBase))
|
||||||
|
import Control.Monad.Catch (MonadCatch (..))
|
||||||
|
#if MIN_VERSION_exceptions(0,6,0)
|
||||||
|
import Control.Monad.Catch (MonadMask (..))
|
||||||
|
#endif
|
||||||
import Control.Monad.IO.Class (MonadIO (liftIO))
|
import Control.Monad.IO.Class (MonadIO (liftIO))
|
||||||
import Control.Monad.Logger (LogLevel, LogSource,
|
import Control.Monad.Logger (LogLevel, LogSource,
|
||||||
MonadLogger (..))
|
MonadLogger (..))
|
||||||
import Control.Monad.Trans.Control (MonadBaseControl (..))
|
import Control.Monad.Trans.Control (MonadBaseControl (..))
|
||||||
import Control.Monad.Trans.Resource (MonadResource (..), InternalState, runInternalState)
|
import Control.Monad.Trans.Resource (MonadResource (..), InternalState, runInternalState, MonadThrow (..), monadThrow, ResourceT)
|
||||||
|
#if !MIN_VERSION_resourcet(1,1,0)
|
||||||
|
import Control.Monad.Trans.Resource (MonadUnsafeIO (..))
|
||||||
|
#endif
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import Data.Conduit (Flush, MonadThrow (..),
|
import Data.Conduit (Flush, Source)
|
||||||
MonadUnsafeIO (..),
|
|
||||||
ResourceT, Source)
|
|
||||||
import Data.Dynamic (Dynamic)
|
import Data.Dynamic (Dynamic)
|
||||||
import Data.IORef (IORef)
|
import Data.IORef (IORef)
|
||||||
import Data.Map (Map, unionWith)
|
import Data.Map (Map, unionWith)
|
||||||
@ -46,7 +52,12 @@ import Network.Wai (FilePart,
|
|||||||
RequestBodyLength)
|
RequestBodyLength)
|
||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import qualified Network.Wai.Parse as NWP
|
import qualified Network.Wai.Parse as NWP
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
import System.Log.FastLogger (LogStr, LoggerSet, toLogStr, pushLogStr)
|
||||||
|
import Network.Wai.Logger (DateCacheGetter)
|
||||||
|
#else
|
||||||
import System.Log.FastLogger (LogStr, Logger, toLogStr)
|
import System.Log.FastLogger (LogStr, Logger, toLogStr)
|
||||||
|
#endif
|
||||||
import Text.Blaze.Html (Html)
|
import Text.Blaze.Html (Html)
|
||||||
import Text.Hamlet (HtmlUrl)
|
import Text.Hamlet (HtmlUrl)
|
||||||
import Text.Julius (JavascriptUrl)
|
import Text.Julius (JavascriptUrl)
|
||||||
@ -54,6 +65,9 @@ import Web.Cookie (SetCookie)
|
|||||||
import Yesod.Core.Internal.Util (getTime, putTime)
|
import Yesod.Core.Internal.Util (getTime, putTime)
|
||||||
import Control.Monad.Trans.Class (MonadTrans (..))
|
import Control.Monad.Trans.Class (MonadTrans (..))
|
||||||
import Yesod.Routes.Class (RenderRoute (..), ParseRoute (..))
|
import Yesod.Routes.Class (RenderRoute (..), ParseRoute (..))
|
||||||
|
import Control.Monad.Reader (MonadReader (..))
|
||||||
|
import Prelude hiding (catch)
|
||||||
|
import Control.DeepSeq (NFData (rnf))
|
||||||
|
|
||||||
-- Sessions
|
-- Sessions
|
||||||
type SessionMap = Map Text ByteString
|
type SessionMap = Map Text ByteString
|
||||||
@ -112,6 +126,7 @@ data YesodRequest = YesodRequest
|
|||||||
-- or a higher-level data structure which Yesod will turn into a @Response@.
|
-- or a higher-level data structure which Yesod will turn into a @Response@.
|
||||||
data YesodResponse
|
data YesodResponse
|
||||||
= YRWai !W.Response
|
= YRWai !W.Response
|
||||||
|
| YRWaiApp !W.Application
|
||||||
| YRPlain !H.Status ![Header] !ContentType !Content !SessionMap
|
| YRPlain !H.Status ![Header] !ContentType !Content !SessionMap
|
||||||
|
|
||||||
-- | A tuple containing both the POST parameters and submitted files.
|
-- | A tuple containing both the POST parameters and submitted files.
|
||||||
@ -128,7 +143,11 @@ data FileInfo = FileInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
data FileUpload = FileUploadMemory !(NWP.BackEnd L.ByteString)
|
data FileUpload = FileUploadMemory !(NWP.BackEnd L.ByteString)
|
||||||
|
#if MIN_VERSION_wai_extra(2, 0, 1)
|
||||||
|
| FileUploadDisk !(InternalState -> NWP.BackEnd FilePath)
|
||||||
|
#else
|
||||||
| FileUploadDisk !(NWP.BackEnd FilePath)
|
| FileUploadDisk !(NWP.BackEnd FilePath)
|
||||||
|
#endif
|
||||||
| FileUploadSource !(NWP.BackEnd (Source (ResourceT IO) ByteString))
|
| FileUploadSource !(NWP.BackEnd (Source (ResourceT IO) ByteString))
|
||||||
|
|
||||||
-- | How to determine the root of the application for constructing URLs.
|
-- | How to determine the root of the application for constructing URLs.
|
||||||
@ -298,6 +317,14 @@ data Header =
|
|||||||
| Header ByteString ByteString
|
| Header ByteString ByteString
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- FIXME In the next major version bump, let's just add strictness annotations
|
||||||
|
-- to Header (and probably everywhere else). We can also add strictness
|
||||||
|
-- annotations to SetCookie in the cookie package.
|
||||||
|
instance NFData Header where
|
||||||
|
rnf (AddCookie x) = rnf x
|
||||||
|
rnf (DeleteCookie x y) = x `seq` y `seq` ()
|
||||||
|
rnf (Header x y) = x `seq` y `seq` ()
|
||||||
|
|
||||||
data Location url = Local url | Remote Text
|
data Location url = Local url | Remote Text
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
@ -346,6 +373,7 @@ data HandlerContents =
|
|||||||
| HCRedirect H.Status Text
|
| HCRedirect H.Status Text
|
||||||
| HCCreated Text
|
| HCCreated Text
|
||||||
| HCWai W.Response
|
| HCWai W.Response
|
||||||
|
| HCWaiApp W.Application
|
||||||
deriving Typeable
|
deriving Typeable
|
||||||
|
|
||||||
instance Show HandlerContents where
|
instance Show HandlerContents where
|
||||||
@ -355,6 +383,7 @@ instance Show HandlerContents where
|
|||||||
show (HCRedirect s t) = "HCRedirect " ++ show (s, t)
|
show (HCRedirect s t) = "HCRedirect " ++ show (s, t)
|
||||||
show (HCCreated t) = "HCCreated " ++ show t
|
show (HCCreated t) = "HCCreated " ++ show t
|
||||||
show (HCWai _) = "HCWai"
|
show (HCWai _) = "HCWai"
|
||||||
|
show (HCWaiApp _) = "HCWaiApp"
|
||||||
instance Exception HandlerContents
|
instance Exception HandlerContents
|
||||||
|
|
||||||
-- Instances for WidgetT
|
-- Instances for WidgetT
|
||||||
@ -375,17 +404,54 @@ instance MonadBase b m => MonadBase b (WidgetT site m) where
|
|||||||
liftBase = WidgetT . const . liftBase . fmap (, mempty)
|
liftBase = WidgetT . const . liftBase . fmap (, mempty)
|
||||||
instance MonadBaseControl b m => MonadBaseControl b (WidgetT site m) where
|
instance MonadBaseControl b m => MonadBaseControl b (WidgetT site m) where
|
||||||
data StM (WidgetT site m) a = StW (StM m (a, GWData (Route site)))
|
data StM (WidgetT site m) a = StW (StM m (a, GWData (Route site)))
|
||||||
liftBaseWith f = WidgetT $ \reader ->
|
liftBaseWith f = WidgetT $ \reader' ->
|
||||||
liftBaseWith $ \runInBase ->
|
liftBaseWith $ \runInBase ->
|
||||||
liftM (\x -> (x, mempty))
|
liftM (\x -> (x, mempty))
|
||||||
(f $ liftM StW . runInBase . flip unWidgetT reader)
|
(f $ liftM StW . runInBase . flip unWidgetT reader')
|
||||||
restoreM (StW base) = WidgetT $ const $ restoreM base
|
restoreM (StW base) = WidgetT $ const $ restoreM base
|
||||||
|
instance Monad m => MonadReader site (WidgetT site m) where
|
||||||
|
ask = WidgetT $ \hd -> return (rheSite $ handlerEnv hd, mempty)
|
||||||
|
local f (WidgetT g) = WidgetT $ \hd -> g hd
|
||||||
|
{ handlerEnv = (handlerEnv hd)
|
||||||
|
{ rheSite = f $ rheSite $ handlerEnv hd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
instance MonadTrans (WidgetT site) where
|
instance MonadTrans (WidgetT site) where
|
||||||
lift = WidgetT . const . liftM (, mempty)
|
lift = WidgetT . const . liftM (, mempty)
|
||||||
instance MonadThrow m => MonadThrow (WidgetT site m) where
|
instance MonadThrow m => MonadThrow (WidgetT site m) where
|
||||||
|
#if MIN_VERSION_resourcet(1,1,0)
|
||||||
|
throwM = lift . throwM
|
||||||
|
|
||||||
|
instance MonadCatch m => MonadCatch (HandlerT site m) where
|
||||||
|
catch (HandlerT m) c = HandlerT $ \r -> m r `catch` \e -> unHandlerT (c e) r
|
||||||
|
#if MIN_VERSION_exceptions(0,6,0)
|
||||||
|
instance MonadMask m => MonadMask (HandlerT site m) where
|
||||||
|
#endif
|
||||||
|
mask a = HandlerT $ \e -> mask $ \u -> unHandlerT (a $ q u) e
|
||||||
|
where q u (HandlerT b) = HandlerT (u . b)
|
||||||
|
uninterruptibleMask a =
|
||||||
|
HandlerT $ \e -> uninterruptibleMask $ \u -> unHandlerT (a $ q u) e
|
||||||
|
where q u (HandlerT b) = HandlerT (u . b)
|
||||||
|
instance MonadCatch m => MonadCatch (WidgetT site m) where
|
||||||
|
catch (WidgetT m) c = WidgetT $ \r -> m r `catch` \e -> unWidgetT (c e) r
|
||||||
|
#if MIN_VERSION_exceptions(0,6,0)
|
||||||
|
instance MonadMask m => MonadMask (WidgetT site m) where
|
||||||
|
#endif
|
||||||
|
mask a = WidgetT $ \e -> mask $ \u -> unWidgetT (a $ q u) e
|
||||||
|
where q u (WidgetT b) = WidgetT (u . b)
|
||||||
|
uninterruptibleMask a =
|
||||||
|
WidgetT $ \e -> uninterruptibleMask $ \u -> unWidgetT (a $ q u) e
|
||||||
|
where q u (WidgetT b) = WidgetT (u . b)
|
||||||
|
#else
|
||||||
monadThrow = lift . monadThrow
|
monadThrow = lift . monadThrow
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MIN_VERSION_resourcet(1,1,0)
|
||||||
|
instance (Applicative m, MonadIO m, MonadBase IO m, MonadThrow m) => MonadResource (WidgetT site m) where
|
||||||
|
#else
|
||||||
instance (Applicative m, MonadIO m, MonadUnsafeIO m, MonadThrow m) => MonadResource (WidgetT site m) where
|
instance (Applicative m, MonadIO m, MonadUnsafeIO m, MonadThrow m) => MonadResource (WidgetT site m) where
|
||||||
|
#endif
|
||||||
liftResourceT f = WidgetT $ \hd -> liftIO $ fmap (, mempty) $ runInternalState f (handlerResource hd)
|
liftResourceT f = WidgetT $ \hd -> liftIO $ fmap (, mempty) $ runInternalState f (handlerResource hd)
|
||||||
|
|
||||||
instance MonadIO m => MonadLogger (WidgetT site m) where
|
instance MonadIO m => MonadLogger (WidgetT site m) where
|
||||||
@ -408,6 +474,13 @@ instance MonadIO m => MonadIO (HandlerT site m) where
|
|||||||
liftIO = lift . liftIO
|
liftIO = lift . liftIO
|
||||||
instance MonadBase b m => MonadBase b (HandlerT site m) where
|
instance MonadBase b m => MonadBase b (HandlerT site m) where
|
||||||
liftBase = lift . liftBase
|
liftBase = lift . liftBase
|
||||||
|
instance Monad m => MonadReader site (HandlerT site m) where
|
||||||
|
ask = HandlerT $ return . rheSite . handlerEnv
|
||||||
|
local f (HandlerT g) = HandlerT $ \hd -> g hd
|
||||||
|
{ handlerEnv = (handlerEnv hd)
|
||||||
|
{ rheSite = f $ rheSite $ handlerEnv hd
|
||||||
|
}
|
||||||
|
}
|
||||||
-- | Note: although we provide a @MonadBaseControl@ instance, @lifted-base@'s
|
-- | Note: although we provide a @MonadBaseControl@ instance, @lifted-base@'s
|
||||||
-- @fork@ function is incompatible with the underlying @ResourceT@ system.
|
-- @fork@ function is incompatible with the underlying @ResourceT@ system.
|
||||||
-- Instead, if you must fork a separate thread, you should use
|
-- Instead, if you must fork a separate thread, you should use
|
||||||
@ -418,14 +491,23 @@ instance MonadBase b m => MonadBase b (HandlerT site m) where
|
|||||||
-- after cleanup. Please contact the maintainers.\"
|
-- after cleanup. Please contact the maintainers.\"
|
||||||
instance MonadBaseControl b m => MonadBaseControl b (HandlerT site m) where
|
instance MonadBaseControl b m => MonadBaseControl b (HandlerT site m) where
|
||||||
data StM (HandlerT site m) a = StH (StM m a)
|
data StM (HandlerT site m) a = StH (StM m a)
|
||||||
liftBaseWith f = HandlerT $ \reader ->
|
liftBaseWith f = HandlerT $ \reader' ->
|
||||||
liftBaseWith $ \runInBase ->
|
liftBaseWith $ \runInBase ->
|
||||||
f $ liftM StH . runInBase . (\(HandlerT r) -> r reader)
|
f $ liftM StH . runInBase . (\(HandlerT r) -> r reader')
|
||||||
restoreM (StH base) = HandlerT $ const $ restoreM base
|
restoreM (StH base) = HandlerT $ const $ restoreM base
|
||||||
|
|
||||||
instance MonadThrow m => MonadThrow (HandlerT site m) where
|
instance MonadThrow m => MonadThrow (HandlerT site m) where
|
||||||
|
#if MIN_VERSION_resourcet(1,1,0)
|
||||||
|
throwM = lift . monadThrow
|
||||||
|
#else
|
||||||
monadThrow = lift . monadThrow
|
monadThrow = lift . monadThrow
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MIN_VERSION_resourcet(1,1,0)
|
||||||
|
instance (MonadIO m, MonadBase IO m, MonadThrow m) => MonadResource (HandlerT site m) where
|
||||||
|
#else
|
||||||
instance (MonadIO m, MonadUnsafeIO m, MonadThrow m) => MonadResource (HandlerT site m) where
|
instance (MonadIO m, MonadUnsafeIO m, MonadThrow m) => MonadResource (HandlerT site m) where
|
||||||
|
#endif
|
||||||
liftResourceT f = HandlerT $ \hd -> liftIO $ runInternalState f (handlerResource hd)
|
liftResourceT f = HandlerT $ \hd -> liftIO $ runInternalState f (handlerResource hd)
|
||||||
|
|
||||||
instance MonadIO m => MonadLogger (HandlerT site m) where
|
instance MonadIO m => MonadLogger (HandlerT site m) where
|
||||||
@ -445,3 +527,13 @@ instance RenderRoute WaiSubsite where
|
|||||||
renderRoute (WaiSubsiteRoute ps qs) = (ps, qs)
|
renderRoute (WaiSubsiteRoute ps qs) = (ps, qs)
|
||||||
instance ParseRoute WaiSubsite where
|
instance ParseRoute WaiSubsite where
|
||||||
parseRoute (x, y) = Just $ WaiSubsiteRoute x y
|
parseRoute (x, y) = Just $ WaiSubsiteRoute x y
|
||||||
|
|
||||||
|
#if MIN_VERSION_fast_logger(2, 0, 0)
|
||||||
|
data Logger = Logger
|
||||||
|
{ loggerSet :: !LoggerSet
|
||||||
|
, loggerDate :: !DateCacheGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerPutStr :: Logger -> LogStr -> IO ()
|
||||||
|
loggerPutStr (Logger ls _) = pushLogStr ls
|
||||||
|
#endif
|
||||||
|
|||||||
@ -47,6 +47,7 @@ module Yesod.Core.Widget
|
|||||||
, handlerToWidget
|
, handlerToWidget
|
||||||
-- * Internal
|
-- * Internal
|
||||||
, whamletFileWithSettings
|
, whamletFileWithSettings
|
||||||
|
, asWidgetT
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
@ -82,10 +83,16 @@ instance render ~ RY site => ToWidget site (render -> Html) where
|
|||||||
toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty
|
toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty
|
||||||
instance render ~ RY site => ToWidget site (render -> Css) where
|
instance render ~ RY site => ToWidget site (render -> Css) where
|
||||||
toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . x
|
toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . x
|
||||||
|
instance ToWidget site Css where
|
||||||
|
toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . const x
|
||||||
instance render ~ RY site => ToWidget site (render -> CssBuilder) where
|
instance render ~ RY site => ToWidget site (render -> CssBuilder) where
|
||||||
toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty
|
toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty
|
||||||
|
instance ToWidget site CssBuilder where
|
||||||
|
toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . const x) mempty mempty
|
||||||
instance render ~ RY site => ToWidget site (render -> Javascript) where
|
instance render ~ RY site => ToWidget site (render -> Javascript) where
|
||||||
toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just x) mempty
|
toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just x) mempty
|
||||||
|
instance ToWidget site Javascript where
|
||||||
|
toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just $ const x) mempty
|
||||||
instance (site' ~ site, IO ~ m, a ~ ()) => ToWidget site' (WidgetT site m a) where
|
instance (site' ~ site, IO ~ m, a ~ ()) => ToWidget site' (WidgetT site m a) where
|
||||||
toWidget = liftWidgetT
|
toWidget = liftWidgetT
|
||||||
instance ToWidget site Html where
|
instance ToWidget site Html where
|
||||||
@ -104,8 +111,12 @@ class ToWidgetMedia site a where
|
|||||||
-> m ()
|
-> m ()
|
||||||
instance render ~ RY site => ToWidgetMedia site (render -> Css) where
|
instance render ~ RY site => ToWidgetMedia site (render -> Css) where
|
||||||
toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . x
|
toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . x
|
||||||
|
instance ToWidgetMedia site Css where
|
||||||
|
toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . const x
|
||||||
instance render ~ RY site => ToWidgetMedia site (render -> CssBuilder) where
|
instance render ~ RY site => ToWidgetMedia site (render -> CssBuilder) where
|
||||||
toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty
|
toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty
|
||||||
|
instance ToWidgetMedia site CssBuilder where
|
||||||
|
toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . const x) mempty mempty
|
||||||
|
|
||||||
class ToWidgetBody site a where
|
class ToWidgetBody site a where
|
||||||
toWidgetBody :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()
|
toWidgetBody :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()
|
||||||
@ -114,6 +125,8 @@ instance render ~ RY site => ToWidgetBody site (render -> Html) where
|
|||||||
toWidgetBody = toWidget
|
toWidgetBody = toWidget
|
||||||
instance render ~ RY site => ToWidgetBody site (render -> Javascript) where
|
instance render ~ RY site => ToWidgetBody site (render -> Javascript) where
|
||||||
toWidgetBody j = toWidget $ \r -> H.script $ preEscapedLazyText $ renderJavascriptUrl r j
|
toWidgetBody j = toWidget $ \r -> H.script $ preEscapedLazyText $ renderJavascriptUrl r j
|
||||||
|
instance ToWidgetBody site Javascript where
|
||||||
|
toWidgetBody j = toWidget $ \_ -> H.script $ preEscapedLazyText $ renderJavascript j
|
||||||
instance ToWidgetBody site Html where
|
instance ToWidgetBody site Html where
|
||||||
toWidgetBody = toWidget
|
toWidgetBody = toWidget
|
||||||
|
|
||||||
@ -124,10 +137,16 @@ instance render ~ RY site => ToWidgetHead site (render -> Html) where
|
|||||||
toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty . Head
|
toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty . Head
|
||||||
instance render ~ RY site => ToWidgetHead site (render -> Css) where
|
instance render ~ RY site => ToWidgetHead site (render -> Css) where
|
||||||
toWidgetHead = toWidget
|
toWidgetHead = toWidget
|
||||||
|
instance ToWidgetHead site Css where
|
||||||
|
toWidgetHead = toWidget
|
||||||
instance render ~ RY site => ToWidgetHead site (render -> CssBuilder) where
|
instance render ~ RY site => ToWidgetHead site (render -> CssBuilder) where
|
||||||
toWidgetHead = toWidget
|
toWidgetHead = toWidget
|
||||||
|
instance ToWidgetHead site CssBuilder where
|
||||||
|
toWidgetHead = toWidget
|
||||||
instance render ~ RY site => ToWidgetHead site (render -> Javascript) where
|
instance render ~ RY site => ToWidgetHead site (render -> Javascript) where
|
||||||
toWidgetHead j = toWidgetHead $ \r -> H.script $ preEscapedLazyText $ renderJavascriptUrl r j
|
toWidgetHead j = toWidgetHead $ \r -> H.script $ preEscapedLazyText $ renderJavascriptUrl r j
|
||||||
|
instance ToWidgetHead site Javascript where
|
||||||
|
toWidgetHead j = toWidgetHead $ \_ -> H.script $ preEscapedLazyText $ renderJavascript j
|
||||||
instance ToWidgetHead site Html where
|
instance ToWidgetHead site Html where
|
||||||
toWidgetHead = toWidgetHead . const
|
toWidgetHead = toWidgetHead . const
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
-- | BigTable benchmark implemented using Hamlet.
|
-- | BigTable benchmark implemented using Hamlet.
|
||||||
--
|
--
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
@ -7,19 +8,22 @@ import Criterion.Main
|
|||||||
import Text.Hamlet
|
import Text.Hamlet
|
||||||
import Numeric (showInt)
|
import Numeric (showInt)
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import qualified Text.Blaze.Renderer.Utf8 as Utf8
|
import qualified Text.Blaze.Html.Renderer.Utf8 as Utf8
|
||||||
import Data.Monoid (mconcat)
|
import Data.Monoid (mconcat)
|
||||||
import Text.Blaze.Html5 (table, tr, td)
|
import Text.Blaze.Html5 (table, tr, td)
|
||||||
import Yesod.Widget
|
import Text.Blaze.Html (toHtml)
|
||||||
|
import Yesod.Core.Widget
|
||||||
import Control.Monad.Trans.Writer
|
import Control.Monad.Trans.Writer
|
||||||
import Control.Monad.Trans.RWS
|
import Control.Monad.Trans.RWS
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import Yesod.Internal
|
import Yesod.Core.Types
|
||||||
|
import Data.Monoid
|
||||||
|
import Data.IORef
|
||||||
|
|
||||||
main = defaultMain
|
main = defaultMain
|
||||||
[ bench "bigTable html" $ nf bigTableHtml bigTableData
|
[ bench "bigTable html" $ nf bigTableHtml bigTableData
|
||||||
, bench "bigTable hamlet" $ nf bigTableHamlet bigTableData
|
, bench "bigTable hamlet" $ nf bigTableHamlet bigTableData
|
||||||
, bench "bigTable widget" $ nf bigTableWidget bigTableData
|
, bench "bigTable widget" $ nfIO (bigTableWidget bigTableData)
|
||||||
, bench "bigTable blaze" $ nf bigTableBlaze bigTableData
|
, bench "bigTable blaze" $ nf bigTableBlaze bigTableData
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
@ -30,50 +34,35 @@ main = defaultMain
|
|||||||
bigTableData = replicate rows [1..10]
|
bigTableData = replicate rows [1..10]
|
||||||
{-# NOINLINE bigTableData #-}
|
{-# NOINLINE bigTableData #-}
|
||||||
|
|
||||||
bigTableHtml rows = L.length $ renderHtml [$hamlet|
|
bigTableHtml rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
|
||||||
<table
|
<table>
|
||||||
$forall row <- rows
|
$forall row <- rows
|
||||||
<tr
|
<tr>
|
||||||
$forall cell <- row
|
$forall cell <- row
|
||||||
<td>#{show cell}
|
<td>#{show cell}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
bigTableHamlet rows = L.length $ renderHamlet id [$hamlet|
|
bigTableHamlet rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
|
||||||
<table
|
<table>
|
||||||
$forall row <- rows
|
$forall row <- rows
|
||||||
<tr
|
<tr>
|
||||||
$forall cell <- row
|
$forall cell <- row
|
||||||
<td>#{show cell}
|
<td>#{show cell}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
bigTableWidget rows = L.length $ renderHtml $ (run [$hamlet|
|
bigTableWidget rows = fmap (L.length . Utf8.renderHtml . ($ render)) (run [whamlet|
|
||||||
<table
|
<table>
|
||||||
$forall row <- rows
|
$forall row <- rows
|
||||||
<tr
|
<tr>
|
||||||
$forall cell <- row
|
$forall cell <- row
|
||||||
<td>#{show cell}
|
<td>#{show cell}
|
||||||
|]) (\_ _ -> "foo")
|
|])
|
||||||
where
|
where
|
||||||
run (GWidget w) =
|
render _ _ = "foo"
|
||||||
let (_, _, GWData (Body x) _ _ _ _ _ _) = runRWS w () 0
|
run (WidgetT w) = do
|
||||||
in x
|
(_, GWData { gwdBody = Body x }) <- w undefined
|
||||||
{-
|
return x
|
||||||
run (GWidget w) = runIdentity $ do
|
|
||||||
w' <- flip evalStateT 0
|
|
||||||
$ runWriterT $ runWriterT $ runWriterT $ runWriterT
|
|
||||||
$ runWriterT $ runWriterT $ runWriterT w
|
|
||||||
let ((((((((),
|
|
||||||
Body body),
|
|
||||||
_),
|
|
||||||
_),
|
|
||||||
_),
|
|
||||||
_),
|
|
||||||
_),
|
|
||||||
_) = w'
|
|
||||||
|
|
||||||
return body
|
bigTableBlaze t = L.length $ Utf8.renderHtml $ table $ mconcat $ map row t
|
||||||
-}
|
|
||||||
|
|
||||||
bigTableBlaze t = L.length $ renderHtml $ table $ mconcat $ map row t
|
|
||||||
where
|
where
|
||||||
row r = tr $ mconcat $ map (td . string . show) r
|
row r = tr $ mconcat $ map (td . toHtml . show) r
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module YesodCoreTest (specs) where
|
module YesodCoreTest (specs) where
|
||||||
|
|
||||||
import YesodCoreTest.CleanPath
|
import YesodCoreTest.CleanPath
|
||||||
@ -14,6 +15,9 @@ import qualified YesodCoreTest.Redirect as Redirect
|
|||||||
import qualified YesodCoreTest.JsLoader as JsLoader
|
import qualified YesodCoreTest.JsLoader as JsLoader
|
||||||
import qualified YesodCoreTest.RequestBodySize as RequestBodySize
|
import qualified YesodCoreTest.RequestBodySize as RequestBodySize
|
||||||
import qualified YesodCoreTest.Json as Json
|
import qualified YesodCoreTest.Json as Json
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
import qualified YesodCoreTest.RawResponse as RawResponse
|
||||||
|
#endif
|
||||||
import qualified YesodCoreTest.Streaming as Streaming
|
import qualified YesodCoreTest.Streaming as Streaming
|
||||||
import qualified YesodCoreTest.Reps as Reps
|
import qualified YesodCoreTest.Reps as Reps
|
||||||
import qualified YesodCoreTest.Auth as Auth
|
import qualified YesodCoreTest.Auth as Auth
|
||||||
@ -37,6 +41,9 @@ specs = do
|
|||||||
JsLoader.specs
|
JsLoader.specs
|
||||||
RequestBodySize.specs
|
RequestBodySize.specs
|
||||||
Json.specs
|
Json.specs
|
||||||
|
#if MIN_VERSION_wai(2, 1, 0)
|
||||||
|
RawResponse.specs
|
||||||
|
#endif
|
||||||
Streaming.specs
|
Streaming.specs
|
||||||
Reps.specs
|
Reps.specs
|
||||||
Auth.specs
|
Auth.specs
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Network.Wai
|
|||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.List (isSuffixOf)
|
import Data.List (isSuffixOf)
|
||||||
|
import qualified Network.HTTP.Types as H
|
||||||
|
|
||||||
data App = App
|
data App = App
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ test method path f = it (method ++ " " ++ path) $ do
|
|||||||
, requestHeaders =
|
, requestHeaders =
|
||||||
if not $ isSuffixOf "json" path then [] else
|
if not $ isSuffixOf "json" path then [] else
|
||||||
[("Accept", S8.pack "application/json")]
|
[("Accept", S8.pack "application/json")]
|
||||||
|
, httpVersion = H.http11
|
||||||
}
|
}
|
||||||
f sres
|
f sres
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module YesodCoreTest.CleanPath (cleanPathTest, Widget) where
|
module YesodCoreTest.CleanPath (cleanPathTest, Widget) where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
@ -32,7 +33,11 @@ instance ParseRoute Subsite where
|
|||||||
parseRoute (x, _) = Just $ SubsiteRoute x
|
parseRoute (x, _) = Just $ SubsiteRoute x
|
||||||
|
|
||||||
instance YesodSubDispatch Subsite master where
|
instance YesodSubDispatch Subsite master where
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
yesodSubDispatch _ req f = f $ responseLBS
|
||||||
|
#else
|
||||||
yesodSubDispatch _ req = return $ responseLBS
|
yesodSubDispatch _ req = return $ responseLBS
|
||||||
|
#endif
|
||||||
status200
|
status200
|
||||||
[ ("Content-Type", "SUBSITE")
|
[ ("Content-Type", "SUBSITE")
|
||||||
] $ L8.pack $ show (pathInfo req)
|
] $ L8.pack $ show (pathInfo req)
|
||||||
|
|||||||
@ -13,6 +13,11 @@ import qualified Data.ByteString.Lazy as L
|
|||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
import Control.Exception (SomeException, try)
|
import Control.Exception (SomeException, try)
|
||||||
import Network.HTTP.Types (mkStatus)
|
import Network.HTTP.Types (mkStatus)
|
||||||
|
import Blaze.ByteString.Builder (Builder, fromByteString, toLazyByteString)
|
||||||
|
import Data.Monoid (mconcat)
|
||||||
|
import Data.Text (Text, pack)
|
||||||
|
import Control.Monad (forM_)
|
||||||
|
import qualified Control.Exception.Lifted as E
|
||||||
|
|
||||||
data App = App
|
data App = App
|
||||||
|
|
||||||
@ -24,6 +29,14 @@ mkYesod "App" [parseRoutes|
|
|||||||
/error-in-body ErrorInBodyR GET
|
/error-in-body ErrorInBodyR GET
|
||||||
/error-in-body-noeval ErrorInBodyNoEvalR GET
|
/error-in-body-noeval ErrorInBodyNoEvalR GET
|
||||||
/override-status OverrideStatusR GET
|
/override-status OverrideStatusR GET
|
||||||
|
/error/#Int ErrorR GET
|
||||||
|
|
||||||
|
-- https://github.com/yesodweb/yesod/issues/658
|
||||||
|
/builder BuilderR GET
|
||||||
|
/file-bad-len FileBadLenR GET
|
||||||
|
/file-bad-name FileBadNameR GET
|
||||||
|
|
||||||
|
/good-builder GoodBuilderR GET
|
||||||
|]
|
|]
|
||||||
|
|
||||||
overrideStatus = mkStatus 15 "OVERRIDE"
|
overrideStatus = mkStatus 15 "OVERRIDE"
|
||||||
@ -74,6 +87,33 @@ getErrorInBodyNoEvalR = fmap DontFullyEvaluate getErrorInBodyR
|
|||||||
getOverrideStatusR :: Handler ()
|
getOverrideStatusR :: Handler ()
|
||||||
getOverrideStatusR = invalidArgs ["OVERRIDE"]
|
getOverrideStatusR = invalidArgs ["OVERRIDE"]
|
||||||
|
|
||||||
|
getBuilderR :: Handler TypedContent
|
||||||
|
getBuilderR = return $ TypedContent "ignored" $ ContentBuilder (error "builder-3.14159") Nothing
|
||||||
|
|
||||||
|
getFileBadLenR :: Handler TypedContent
|
||||||
|
getFileBadLenR = return $ TypedContent "ignored" $ ContentFile "yesod-core.cabal" (error "filebadlen")
|
||||||
|
|
||||||
|
getFileBadNameR :: Handler TypedContent
|
||||||
|
getFileBadNameR = return $ TypedContent "ignored" $ ContentFile (error "filebadname") Nothing
|
||||||
|
|
||||||
|
goodBuilderContent :: Builder
|
||||||
|
goodBuilderContent = mconcat $ replicate 100 $ fromByteString "This is a test\n"
|
||||||
|
|
||||||
|
getGoodBuilderR :: Handler TypedContent
|
||||||
|
getGoodBuilderR = return $ TypedContent "text/plain" $ toContent goodBuilderContent
|
||||||
|
|
||||||
|
getErrorR :: Int -> Handler ()
|
||||||
|
getErrorR 1 = setSession undefined "foo"
|
||||||
|
getErrorR 2 = setSession "foo" undefined
|
||||||
|
getErrorR 3 = deleteSession undefined
|
||||||
|
getErrorR 4 = addHeader undefined "foo"
|
||||||
|
getErrorR 5 = addHeader "foo" undefined
|
||||||
|
getErrorR 6 = expiresAt undefined
|
||||||
|
getErrorR 7 = setLanguage undefined
|
||||||
|
getErrorR 8 = cacheSeconds undefined
|
||||||
|
getErrorR 9 = setUltDest (undefined :: Text)
|
||||||
|
getErrorR 10 = setMessage undefined
|
||||||
|
|
||||||
errorHandlingTest :: Spec
|
errorHandlingTest :: Spec
|
||||||
errorHandlingTest = describe "Test.ErrorHandling" $ do
|
errorHandlingTest = describe "Test.ErrorHandling" $ do
|
||||||
it "says not found" caseNotFound
|
it "says not found" caseNotFound
|
||||||
@ -82,8 +122,13 @@ errorHandlingTest = describe "Test.ErrorHandling" $ do
|
|||||||
it "error in body == 500" caseErrorInBody
|
it "error in body == 500" caseErrorInBody
|
||||||
it "error in body, no eval == 200" caseErrorInBodyNoEval
|
it "error in body, no eval == 200" caseErrorInBodyNoEval
|
||||||
it "can override status code" caseOverrideStatus
|
it "can override status code" caseOverrideStatus
|
||||||
|
it "builder" caseBuilder
|
||||||
|
it "file with bad len" caseFileBadLen
|
||||||
|
it "file with bad name" caseFileBadName
|
||||||
|
it "builder includes content-length" caseGoodBuilder
|
||||||
|
forM_ [1..10] $ \i -> it ("error case " ++ show i) (caseError i)
|
||||||
|
|
||||||
runner :: Session () -> IO ()
|
runner :: Session a -> IO a
|
||||||
runner f = toWaiApp App >>= runSession f
|
runner f = toWaiApp App >>= runSession f
|
||||||
|
|
||||||
caseNotFound :: IO ()
|
caseNotFound :: IO ()
|
||||||
@ -130,13 +175,45 @@ caseErrorInBody = runner $ do
|
|||||||
caseErrorInBodyNoEval :: IO ()
|
caseErrorInBodyNoEval :: IO ()
|
||||||
caseErrorInBodyNoEval = do
|
caseErrorInBodyNoEval = do
|
||||||
eres <- try $ runner $ do
|
eres <- try $ runner $ do
|
||||||
_ <- request defaultRequest { pathInfo = ["error-in-body-noeval"] }
|
request defaultRequest { pathInfo = ["error-in-body-noeval"] }
|
||||||
return ()
|
|
||||||
case eres of
|
case eres of
|
||||||
Left (_ :: SomeException) -> return ()
|
Left (_ :: SomeException) -> return ()
|
||||||
Right _ -> error "Expected an exception"
|
Right x -> error $ "Expected an exception, got: " ++ show x
|
||||||
|
|
||||||
caseOverrideStatus :: IO ()
|
caseOverrideStatus :: IO ()
|
||||||
caseOverrideStatus = runner $ do
|
caseOverrideStatus = runner $ do
|
||||||
res <- request defaultRequest { pathInfo = ["override-status"] }
|
res <- request defaultRequest { pathInfo = ["override-status"] }
|
||||||
assertStatus 15 res
|
assertStatus 15 res
|
||||||
|
|
||||||
|
caseBuilder :: IO ()
|
||||||
|
caseBuilder = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["builder"] }
|
||||||
|
assertStatus 500 res
|
||||||
|
assertBodyContains "builder-3.14159" res
|
||||||
|
|
||||||
|
caseFileBadLen :: IO ()
|
||||||
|
caseFileBadLen = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["file-bad-len"] }
|
||||||
|
assertStatus 500 res
|
||||||
|
assertBodyContains "filebadlen" res
|
||||||
|
|
||||||
|
caseFileBadName :: IO ()
|
||||||
|
caseFileBadName = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["file-bad-name"] }
|
||||||
|
assertStatus 500 res
|
||||||
|
assertBodyContains "filebadname" res
|
||||||
|
|
||||||
|
caseGoodBuilder :: IO ()
|
||||||
|
caseGoodBuilder = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["good-builder"] }
|
||||||
|
assertStatus 200 res
|
||||||
|
let lbs = toLazyByteString goodBuilderContent
|
||||||
|
assertBody lbs res
|
||||||
|
assertHeader "content-length" (S8.pack $ show $ L.length lbs) res
|
||||||
|
|
||||||
|
caseError :: Int -> IO ()
|
||||||
|
caseError i = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["error", pack $ show i] }
|
||||||
|
assertStatus 500 res `E.catch` \e -> do
|
||||||
|
liftIO $ print res
|
||||||
|
E.throwIO (e :: E.SomeException)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import Data.Monoid (mempty)
|
|||||||
import Data.Map (singleton)
|
import Data.Map (singleton)
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Data.Word (Word64)
|
import Data.Word (Word64)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
randomStringSpecs :: Spec
|
randomStringSpecs :: Spec
|
||||||
randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do
|
randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do
|
||||||
@ -36,7 +37,7 @@ parseWaiRequest' :: Request
|
|||||||
-> Bool
|
-> Bool
|
||||||
-> Word64
|
-> Word64
|
||||||
-> YesodRequest
|
-> YesodRequest
|
||||||
parseWaiRequest' a b c d =
|
parseWaiRequest' a b c d = unsafePerformIO $ -- ugly hack, just to ease migration, should be removed
|
||||||
case parseWaiRequest a b c (Just d) of
|
case parseWaiRequest a b c (Just d) of
|
||||||
Left yreq -> yreq
|
Left yreq -> yreq
|
||||||
Right needGen -> needGen g
|
Right needGen -> needGen g
|
||||||
|
|||||||
@ -19,7 +19,7 @@ instance Yesod App
|
|||||||
|
|
||||||
getHomeR :: Handler RepPlain
|
getHomeR :: Handler RepPlain
|
||||||
getHomeR = do
|
getHomeR = do
|
||||||
val <- parseJsonBody_
|
val <- requireJsonBody
|
||||||
case Map.lookup ("foo" :: Text) val of
|
case Map.lookup ("foo" :: Text) val of
|
||||||
Nothing -> invalidArgs ["foo not found"]
|
Nothing -> invalidArgs ["foo not found"]
|
||||||
Just foo -> return $ RepPlain $ toContent (foo :: Text)
|
Just foo -> return $ RepPlain $ toContent (foo :: Text)
|
||||||
|
|||||||
110
yesod-core/test/YesodCoreTest/RawResponse.hs
Normal file
110
yesod-core/test/YesodCoreTest/RawResponse.hs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses, ScopedTypeVariables #-}
|
||||||
|
module YesodCoreTest.RawResponse (specs, Widget) where
|
||||||
|
|
||||||
|
import Yesod.Core
|
||||||
|
import Test.Hspec
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Network.Wai.Test
|
||||||
|
import Network.Wai (responseStream)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.ByteString.Lazy (ByteString)
|
||||||
|
import qualified Data.Conduit.List as CL
|
||||||
|
import qualified Data.ByteString.Char8 as S8
|
||||||
|
import Data.Conduit
|
||||||
|
import qualified Data.Conduit.Binary as CB
|
||||||
|
import Data.Char (toUpper)
|
||||||
|
import Control.Exception (try, IOException)
|
||||||
|
import Data.Conduit.Network
|
||||||
|
import Network.Socket (sClose)
|
||||||
|
import Control.Concurrent (threadDelay)
|
||||||
|
import Control.Concurrent.Async (withAsync)
|
||||||
|
import Control.Monad.Trans.Resource (register)
|
||||||
|
import Data.IORef
|
||||||
|
import Data.Streaming.Network (bindPortTCP)
|
||||||
|
import Network.HTTP.Types (status200)
|
||||||
|
import Blaze.ByteString.Builder (fromByteString)
|
||||||
|
|
||||||
|
data App = App
|
||||||
|
|
||||||
|
mkYesod "App" [parseRoutes|
|
||||||
|
/ HomeR GET
|
||||||
|
/wai-stream WaiStreamR GET
|
||||||
|
/wai-app-stream WaiAppStreamR GET
|
||||||
|
|]
|
||||||
|
|
||||||
|
instance Yesod App
|
||||||
|
|
||||||
|
getHomeR :: Handler ()
|
||||||
|
getHomeR = do
|
||||||
|
ref <- liftIO $ newIORef 0
|
||||||
|
_ <- register $ writeIORef ref 1
|
||||||
|
sendRawResponse $ \src sink -> liftIO $ do
|
||||||
|
val <- readIORef ref
|
||||||
|
yield (S8.pack $ show val) $$ sink
|
||||||
|
src $$ CL.map (S8.map toUpper) =$ sink
|
||||||
|
|
||||||
|
getWaiStreamR :: Handler ()
|
||||||
|
getWaiStreamR = sendWaiResponse $ responseStream status200 [] $ \send flush -> do
|
||||||
|
flush
|
||||||
|
send $ fromByteString "hello"
|
||||||
|
flush
|
||||||
|
send $ fromByteString " world"
|
||||||
|
|
||||||
|
getWaiAppStreamR :: Handler ()
|
||||||
|
getWaiAppStreamR = sendWaiApplication $ \_ f -> f $ responseStream status200 [] $ \send flush -> do
|
||||||
|
flush
|
||||||
|
send $ fromByteString "hello"
|
||||||
|
flush
|
||||||
|
send $ fromByteString " world"
|
||||||
|
|
||||||
|
getFreePort :: IO Int
|
||||||
|
getFreePort = do
|
||||||
|
loop 43124
|
||||||
|
where
|
||||||
|
loop port = do
|
||||||
|
esocket <- try $ bindPortTCP port "*"
|
||||||
|
case esocket of
|
||||||
|
Left (_ :: IOException) -> loop (succ port)
|
||||||
|
Right socket -> do
|
||||||
|
sClose socket
|
||||||
|
return port
|
||||||
|
|
||||||
|
specs :: Spec
|
||||||
|
specs = do
|
||||||
|
describe "RawResponse" $ do
|
||||||
|
it "works" $ do
|
||||||
|
port <- getFreePort
|
||||||
|
withAsync (warp port App) $ \_ -> do
|
||||||
|
threadDelay 100000
|
||||||
|
runTCPClient (clientSettings port "127.0.0.1") $ \ad -> do
|
||||||
|
yield "GET / HTTP/1.1\r\n\r\nhello" $$ appSink ad
|
||||||
|
(appSource ad $$ CB.take 6) >>= (`shouldBe` "0HELLO")
|
||||||
|
yield "WORLd" $$ appSink ad
|
||||||
|
(appSource ad $$ await) >>= (`shouldBe` Just "WORLD")
|
||||||
|
|
||||||
|
let body req = do
|
||||||
|
port <- getFreePort
|
||||||
|
withAsync (warp port App) $ \_ -> do
|
||||||
|
threadDelay 100000
|
||||||
|
runTCPClient (clientSettings port "127.0.0.1") $ \ad -> do
|
||||||
|
yield req $$ appSink ad
|
||||||
|
appSource ad $$ CB.lines =$ do
|
||||||
|
let loop = do
|
||||||
|
x <- await
|
||||||
|
case x of
|
||||||
|
Nothing -> return ()
|
||||||
|
Just "\r" -> return ()
|
||||||
|
_ -> loop
|
||||||
|
loop
|
||||||
|
|
||||||
|
Just "0005\r" <- await
|
||||||
|
Just "hello\r" <- await
|
||||||
|
|
||||||
|
Just "0006\r" <- await
|
||||||
|
Just " world\r" <- await
|
||||||
|
|
||||||
|
return ()
|
||||||
|
it "sendWaiResponse + responseStream" $ do
|
||||||
|
body "GET /wai-stream HTTP/1.1\r\n\r\n"
|
||||||
|
it "sendWaiApplication + responseStream" $ do
|
||||||
|
body "GET /wai-app-stream HTTP/1.1\r\n\r\n"
|
||||||
@ -7,7 +7,7 @@ import qualified Network.HTTP.Types as H
|
|||||||
|
|
||||||
data Y = Y
|
data Y = Y
|
||||||
mkYesod "Y" [parseRoutes|
|
mkYesod "Y" [parseRoutes|
|
||||||
/ RootR GET
|
/ RootR GET POST
|
||||||
/r301 R301 GET
|
/r301 R301 GET
|
||||||
/r303 R303 GET
|
/r303 R303 GET
|
||||||
/r307 R307 GET
|
/r307 R307 GET
|
||||||
@ -20,6 +20,9 @@ app = yesod Y
|
|||||||
getRootR :: Handler ()
|
getRootR :: Handler ()
|
||||||
getRootR = return ()
|
getRootR = return ()
|
||||||
|
|
||||||
|
postRootR :: Handler ()
|
||||||
|
postRootR = return ()
|
||||||
|
|
||||||
getR301, getR303, getR307, getRRegular :: Handler ()
|
getR301, getR303, getR307, getRRegular :: Handler ()
|
||||||
getR301 = redirectWith H.status301 RootR
|
getR301 = redirectWith H.status301 RootR
|
||||||
getR303 = redirectWith H.status303 RootR
|
getR303 = redirectWith H.status303 RootR
|
||||||
@ -28,6 +31,11 @@ getRRegular = redirect RootR
|
|||||||
|
|
||||||
specs :: Spec
|
specs :: Spec
|
||||||
specs = describe "Redirect" $ do
|
specs = describe "Redirect" $ do
|
||||||
|
it "no redirect" $ app $ do
|
||||||
|
res <- request defaultRequest { pathInfo = [], requestMethod = "POST" }
|
||||||
|
assertStatus 200 res
|
||||||
|
assertBodyContains "" res
|
||||||
|
|
||||||
it "301 redirect" $ app $ do
|
it "301 redirect" $ app $ do
|
||||||
res <- request defaultRequest { pathInfo = ["r301"] }
|
res <- request defaultRequest { pathInfo = ["r301"] }
|
||||||
assertStatus 301 res
|
assertStatus 301 res
|
||||||
@ -45,7 +53,8 @@ specs = describe "Redirect" $ do
|
|||||||
|
|
||||||
it "303 redirect for regular, HTTP 1.1" $ app $ do
|
it "303 redirect for regular, HTTP 1.1" $ app $ do
|
||||||
res <- request defaultRequest {
|
res <- request defaultRequest {
|
||||||
pathInfo = ["rregular"]
|
pathInfo = ["rregular"],
|
||||||
|
httpVersion = H.http11
|
||||||
}
|
}
|
||||||
assertStatus 303 res
|
assertStatus 303 res
|
||||||
assertBodyContains "" res
|
assertBodyContains "" res
|
||||||
|
|||||||
@ -83,8 +83,8 @@ specs :: Spec
|
|||||||
specs = describe "Test.RequestBodySize" $ do
|
specs = describe "Test.RequestBodySize" $ do
|
||||||
caseHelper "lookupPostParam- large" "post" "foobarbaz=bin" 413 413
|
caseHelper "lookupPostParam- large" "post" "foobarbaz=bin" 413 413
|
||||||
caseHelper "lookupPostParam- small" "post" "foo=bin" 200 200
|
caseHelper "lookupPostParam- small" "post" "foo=bin" 200 200
|
||||||
caseHelper "consume- large" "consume" "this is longer than 10" 413 413
|
caseHelper "total consume- large" "consume" "this is longer than 10" 413 413
|
||||||
caseHelper "consume- small" "consume" "smaller" 200 200
|
caseHelper "total consume- small" "consume" "smaller" 200 200
|
||||||
caseHelper "partial consume- large" "partial-consume" "this is longer than 10" 200 413
|
caseHelper "partial consume- large" "partial-consume" "this is longer than 10" 200 413
|
||||||
caseHelper "partial consume- small" "partial-consume" "smaller" 200 200
|
caseHelper "partial consume- small" "partial-consume" "smaller" 200 200
|
||||||
caseHelper "unused- large" "unused" "this is longer than 10" 200 413
|
caseHelper "unused- large" "unused" "this is longer than 10" 200 413
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, MultiParamTypeClasses, OverloadedStrings #-}
|
{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, TypeFamilies, MultiParamTypeClasses, OverloadedStrings #-}
|
||||||
module YesodCoreTest.WaiSubsite (specs, Widget) where
|
module YesodCoreTest.WaiSubsite (specs, Widget) where
|
||||||
|
|
||||||
import YesodCoreTest.YesodTest
|
import YesodCoreTest.YesodTest
|
||||||
@ -6,7 +6,11 @@ import Yesod.Core
|
|||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
|
|
||||||
myApp :: Application
|
myApp :: Application
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
myApp _ f = f $ responseLBS H.status200 [("Content-type", "text/plain")] "WAI"
|
||||||
|
#else
|
||||||
myApp _ = return $ responseLBS H.status200 [("Content-type", "text/plain")] "WAI"
|
myApp _ = return $ responseLBS H.status200 [("Content-type", "text/plain")] "WAI"
|
||||||
|
#endif
|
||||||
|
|
||||||
getApp :: a -> WaiSubsite
|
getApp :: a -> WaiSubsite
|
||||||
getApp _ = WaiSubsite myApp
|
getApp _ = WaiSubsite myApp
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-core
|
name: yesod-core
|
||||||
version: 1.2.4
|
version: 1.2.17
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -26,28 +26,28 @@ library
|
|||||||
build-depends: base >= 4.3 && < 5
|
build-depends: base >= 4.3 && < 5
|
||||||
, time >= 1.1.4
|
, time >= 1.1.4
|
||||||
, yesod-routes >= 1.2 && < 1.3
|
, yesod-routes >= 1.2 && < 1.3
|
||||||
, wai >= 1.4 && < 1.5
|
, wai >= 1.4
|
||||||
, wai-extra >= 1.3 && < 1.4
|
, wai-extra >= 1.3
|
||||||
, bytestring >= 0.9.1.4
|
, bytestring >= 0.9.1.4
|
||||||
, text >= 0.7 && < 0.12
|
, text >= 0.7
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, path-pieces >= 0.1.2 && < 0.2
|
, path-pieces >= 0.1.2 && < 0.2
|
||||||
, hamlet >= 1.1 && < 1.2
|
, hamlet >= 1.1
|
||||||
, shakespeare >= 1.0 && < 1.1
|
, shakespeare >= 1.0 && < 2.1
|
||||||
, shakespeare-js >= 1.0.2 && < 1.2
|
, shakespeare-js >= 1.0.2
|
||||||
, shakespeare-css >= 1.0 && < 1.1
|
, shakespeare-css >= 1.0
|
||||||
, shakespeare-i18n >= 1.0 && < 1.1
|
, shakespeare-i18n >= 1.0
|
||||||
, blaze-builder >= 0.2.1.4 && < 0.4
|
, blaze-builder >= 0.2.1.4 && < 0.4
|
||||||
, transformers >= 0.2.2 && < 0.4
|
, transformers >= 0.2.2
|
||||||
|
, mtl
|
||||||
, clientsession >= 0.9 && < 0.10
|
, clientsession >= 0.9 && < 0.10
|
||||||
, random >= 1.0.0.2 && < 1.1
|
, random >= 1.0.0.2 && < 1.1
|
||||||
, cereal >= 0.3 && < 0.4
|
, cereal >= 0.3
|
||||||
, old-locale >= 1.0.0.2 && < 1.1
|
, old-locale >= 1.0.0.2 && < 1.1
|
||||||
, failure >= 0.2 && < 0.3
|
|
||||||
, containers >= 0.2
|
, containers >= 0.2
|
||||||
, monad-control >= 0.3 && < 0.4
|
, monad-control >= 0.3 && < 0.4
|
||||||
, transformers-base >= 0.4
|
, transformers-base >= 0.4
|
||||||
, cookie >= 0.4 && < 0.5
|
, cookie >= 0.4.1 && < 0.5
|
||||||
, http-types >= 0.7
|
, http-types >= 0.7
|
||||||
, case-insensitive >= 0.2
|
, case-insensitive >= 0.2
|
||||||
, parsec >= 2 && < 3.2
|
, parsec >= 2 && < 3.2
|
||||||
@ -55,9 +55,10 @@ library
|
|||||||
, vector >= 0.9 && < 0.11
|
, vector >= 0.9 && < 0.11
|
||||||
, aeson >= 0.5
|
, aeson >= 0.5
|
||||||
, fast-logger >= 0.2
|
, fast-logger >= 0.2
|
||||||
|
, wai-logger >= 0.2
|
||||||
, monad-logger >= 0.3.1 && < 0.4
|
, monad-logger >= 0.3.1 && < 0.4
|
||||||
, conduit >= 0.5
|
, conduit >= 1.0.11
|
||||||
, resourcet >= 0.4.6 && < 0.5
|
, resourcet >= 0.4.9 && < 1.2
|
||||||
, lifted-base >= 0.1.2
|
, lifted-base >= 0.1.2
|
||||||
, attoparsec-conduit
|
, attoparsec-conduit
|
||||||
, blaze-html >= 0.5
|
, blaze-html >= 0.5
|
||||||
@ -65,6 +66,10 @@ library
|
|||||||
, data-default
|
, data-default
|
||||||
, safe
|
, safe
|
||||||
, warp >= 1.3.8
|
, warp >= 1.3.8
|
||||||
|
, unix-compat
|
||||||
|
, conduit-extra
|
||||||
|
, exceptions
|
||||||
|
, deepseq
|
||||||
|
|
||||||
exposed-modules: Yesod.Core
|
exposed-modules: Yesod.Core
|
||||||
Yesod.Core.Content
|
Yesod.Core.Content
|
||||||
@ -91,6 +96,9 @@ library
|
|||||||
-- This looks like a GHC bug
|
-- This looks like a GHC bug
|
||||||
extensions: MultiParamTypeClasses
|
extensions: MultiParamTypeClasses
|
||||||
|
|
||||||
|
-- Workaround for: http://ghc.haskell.org/trac/ghc/ticket/8443
|
||||||
|
extensions: TemplateHaskell
|
||||||
|
|
||||||
test-suite tests
|
test-suite tests
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
main-is: test.hs
|
main-is: test.hs
|
||||||
@ -100,7 +108,7 @@ test-suite tests
|
|||||||
build-depends: base
|
build-depends: base
|
||||||
,hspec >= 1.3
|
,hspec >= 1.3
|
||||||
,wai-test >= 1.3.0.5
|
,wai-test >= 1.3.0.5
|
||||||
,wai
|
,wai >= 3.0
|
||||||
,yesod-core
|
,yesod-core
|
||||||
,bytestring
|
,bytestring
|
||||||
,hamlet
|
,hamlet
|
||||||
@ -117,7 +125,29 @@ test-suite tests
|
|||||||
, containers
|
, containers
|
||||||
, lifted-base
|
, lifted-base
|
||||||
, resourcet
|
, resourcet
|
||||||
|
, network-conduit
|
||||||
|
, network
|
||||||
|
, async
|
||||||
|
, conduit-extra
|
||||||
|
, shakespeare
|
||||||
|
, streaming-commons
|
||||||
|
, wai-extra
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
extensions: TemplateHaskell
|
||||||
|
|
||||||
|
benchmark widgets
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
hs-source-dirs: bench
|
||||||
|
build-depends: base
|
||||||
|
, criterion
|
||||||
|
, bytestring
|
||||||
|
, text
|
||||||
|
, hamlet
|
||||||
|
, transformers
|
||||||
|
, yesod-core
|
||||||
|
, blaze-html
|
||||||
|
main-is: widget.hs
|
||||||
|
ghc-options: -Wall -O2
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
@ -46,7 +46,18 @@ repEventSource :: (EventSourcePolyfill -> C.Source (HandlerT site IO) ES.ServerE
|
|||||||
-> HandlerT site IO TypedContent
|
-> HandlerT site IO TypedContent
|
||||||
repEventSource src =
|
repEventSource src =
|
||||||
prepareForEventSource >>=
|
prepareForEventSource >>=
|
||||||
respondEventStream . ES.sourceToSource . src
|
respondEventStream . sourceToSource . src
|
||||||
|
|
||||||
|
-- | Convert a ServerEvent source into a Builder source of serialized
|
||||||
|
-- events.
|
||||||
|
sourceToSource :: Monad m => C.Source m ES.ServerEvent -> C.Source m (C.Flush Builder)
|
||||||
|
sourceToSource src =
|
||||||
|
src C.$= C.awaitForever eventToFlushBuilder
|
||||||
|
where
|
||||||
|
eventToFlushBuilder event =
|
||||||
|
case ES.eventToBuilder event of
|
||||||
|
Nothing -> return ()
|
||||||
|
Just x -> C.yield (C.Chunk x) >> C.yield C.Flush
|
||||||
|
|
||||||
|
|
||||||
-- | Return a Server-Sent Event stream given a 'HandlerT' action
|
-- | Return a Server-Sent Event stream given a 'HandlerT' action
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-eventsource
|
name: yesod-eventsource
|
||||||
version: 1.1
|
version: 1.1.1
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Felipe Lessa <felipe.lessa@gmail.com>
|
author: Felipe Lessa <felipe.lessa@gmail.com>
|
||||||
@ -29,9 +29,10 @@ description:
|
|||||||
library
|
library
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, yesod-core == 1.2.*
|
, yesod-core == 1.2.*
|
||||||
, conduit >= 0.5 && < 1.1
|
, conduit >= 0.5 && < 1.2
|
||||||
, wai >= 1.3 && < 1.5
|
, wai >= 1.3
|
||||||
, wai-eventsource >= 1.3 && < 1.4
|
, wai-eventsource >= 1.3
|
||||||
|
, wai-extra
|
||||||
, blaze-builder
|
, blaze-builder
|
||||||
, transformers
|
, transformers
|
||||||
exposed-modules: Yesod.EventSource
|
exposed-modules: Yesod.EventSource
|
||||||
|
|||||||
262
yesod-form/Yesod/Form/Bootstrap3.hs
Normal file
262
yesod-form/Yesod/Form/Bootstrap3.hs
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- | Helper functions for creating forms when using Bootstrap v3.
|
||||||
|
module Yesod.Form.Bootstrap3
|
||||||
|
( -- * Rendering forms
|
||||||
|
renderBootstrap3
|
||||||
|
, BootstrapFormLayout(..)
|
||||||
|
, BootstrapGridOptions(..)
|
||||||
|
-- * Field settings
|
||||||
|
, bfs
|
||||||
|
, withPlaceholder
|
||||||
|
, withAutofocus
|
||||||
|
, withLargeInput
|
||||||
|
, withSmallInput
|
||||||
|
-- * Submit button
|
||||||
|
, bootstrapSubmit
|
||||||
|
, mbootstrapSubmit
|
||||||
|
, BootstrapSubmit(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Arrow (second)
|
||||||
|
import Control.Monad (liftM)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.String (IsString(..))
|
||||||
|
import Yesod.Core
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
import Yesod.Form.Types
|
||||||
|
import Yesod.Form.Functions
|
||||||
|
|
||||||
|
-- | Create a new 'FieldSettings' with the classes that are
|
||||||
|
-- required by Bootstrap v3.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
bfs :: RenderMessage site msg => msg -> FieldSettings site
|
||||||
|
bfs msg =
|
||||||
|
FieldSettings (SomeMessage msg) Nothing Nothing Nothing [("class", "form-control")]
|
||||||
|
|
||||||
|
|
||||||
|
-- | Add a placeholder attribute to a field. If you need i18n
|
||||||
|
-- for the placeholder, currently you\'ll need to do a hack and
|
||||||
|
-- use 'getMessageRender' manually.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
withPlaceholder :: Text -> FieldSettings site -> FieldSettings site
|
||||||
|
withPlaceholder placeholder fs = fs { fsAttrs = newAttrs }
|
||||||
|
where newAttrs = ("placeholder", placeholder) : fsAttrs fs
|
||||||
|
|
||||||
|
|
||||||
|
-- | Add an autofocus attribute to a field.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
withAutofocus :: FieldSettings site -> FieldSettings site
|
||||||
|
withAutofocus fs = fs { fsAttrs = newAttrs }
|
||||||
|
where newAttrs = ("autofocus", "autofocus") : fsAttrs fs
|
||||||
|
|
||||||
|
|
||||||
|
-- | Add the @input-lg@ CSS class to a field.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
withLargeInput :: FieldSettings site -> FieldSettings site
|
||||||
|
withLargeInput fs = fs { fsAttrs = newAttrs }
|
||||||
|
where newAttrs = addClass "input-lg" (fsAttrs fs)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Add the @input-sm@ CSS class to a field.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
withSmallInput :: FieldSettings site -> FieldSettings site
|
||||||
|
withSmallInput fs = fs { fsAttrs = newAttrs }
|
||||||
|
where newAttrs = addClass "input-sm" (fsAttrs fs)
|
||||||
|
|
||||||
|
|
||||||
|
addClass :: Text -> [(Text, Text)] -> [(Text, Text)]
|
||||||
|
addClass klass [] = [("class", klass)]
|
||||||
|
addClass klass (("class", old):rest) = ("class", T.concat [old, " ", klass]) : rest
|
||||||
|
addClass klass (other :rest) = other : addClass klass rest
|
||||||
|
|
||||||
|
|
||||||
|
-- | How many bootstrap grid columns should be taken (see
|
||||||
|
-- 'BootstrapFormLayout').
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
data BootstrapGridOptions =
|
||||||
|
ColXs !Int
|
||||||
|
| ColSm !Int
|
||||||
|
| ColMd !Int
|
||||||
|
| ColLg !Int
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
toColumn :: BootstrapGridOptions -> String
|
||||||
|
toColumn (ColXs 0) = ""
|
||||||
|
toColumn (ColSm 0) = ""
|
||||||
|
toColumn (ColMd 0) = ""
|
||||||
|
toColumn (ColLg 0) = ""
|
||||||
|
toColumn (ColXs columns) = "col-xs-" ++ show columns
|
||||||
|
toColumn (ColSm columns) = "col-sm-" ++ show columns
|
||||||
|
toColumn (ColMd columns) = "col-md-" ++ show columns
|
||||||
|
toColumn (ColLg columns) = "col-lg-" ++ show columns
|
||||||
|
|
||||||
|
toOffset :: BootstrapGridOptions -> String
|
||||||
|
toOffset (ColXs 0) = ""
|
||||||
|
toOffset (ColSm 0) = ""
|
||||||
|
toOffset (ColMd 0) = ""
|
||||||
|
toOffset (ColLg 0) = ""
|
||||||
|
toOffset (ColXs columns) = "col-xs-offset-" ++ show columns
|
||||||
|
toOffset (ColSm columns) = "col-sm-offset-" ++ show columns
|
||||||
|
toOffset (ColMd columns) = "col-md-offset-" ++ show columns
|
||||||
|
toOffset (ColLg columns) = "col-lg-offset-" ++ show columns
|
||||||
|
|
||||||
|
addGO :: BootstrapGridOptions -> BootstrapGridOptions -> BootstrapGridOptions
|
||||||
|
addGO (ColXs a) (ColXs b) = ColXs (a+b)
|
||||||
|
addGO (ColSm a) (ColSm b) = ColSm (a+b)
|
||||||
|
addGO (ColMd a) (ColMd b) = ColMd (a+b)
|
||||||
|
addGO (ColLg a) (ColLg b) = ColLg (a+b)
|
||||||
|
addGO a b | a > b = addGO b a
|
||||||
|
addGO (ColXs a) other = addGO (ColSm a) other
|
||||||
|
addGO (ColSm a) other = addGO (ColMd a) other
|
||||||
|
addGO (ColMd a) other = addGO (ColLg a) other
|
||||||
|
addGO (ColLg _) _ = error "Yesod.Form.Bootstrap.addGO: never here"
|
||||||
|
|
||||||
|
|
||||||
|
-- | The layout used for the bootstrap form.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
data BootstrapFormLayout =
|
||||||
|
BootstrapBasicForm
|
||||||
|
| BootstrapInlineForm
|
||||||
|
| BootstrapHorizontalForm
|
||||||
|
{ bflLabelOffset :: !BootstrapGridOptions
|
||||||
|
, bflLabelSize :: !BootstrapGridOptions
|
||||||
|
, bflInputOffset :: !BootstrapGridOptions
|
||||||
|
, bflInputSize :: !BootstrapGridOptions
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Render the given form using Bootstrap v3 conventions.
|
||||||
|
--
|
||||||
|
-- Sample Hamlet for 'BootstrapHorizontalForm':
|
||||||
|
--
|
||||||
|
-- > <form .form-horizontal role=form method=post action=@{ActionR} enctype=#{formEnctype}>
|
||||||
|
-- > ^{formWidget}
|
||||||
|
-- > ^{bootstrapSubmit MsgSubmit}
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
renderBootstrap3 :: Monad m => BootstrapFormLayout -> FormRender m a
|
||||||
|
renderBootstrap3 formLayout aform fragment = do
|
||||||
|
(res, views') <- aFormToForm aform
|
||||||
|
let views = views' []
|
||||||
|
has (Just _) = True
|
||||||
|
has Nothing = False
|
||||||
|
widget = [whamlet|
|
||||||
|
$newline never
|
||||||
|
#{fragment}
|
||||||
|
$forall view <- views
|
||||||
|
<div .form-group :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.has-error>
|
||||||
|
$case formLayout
|
||||||
|
$of BootstrapBasicForm
|
||||||
|
$if fvId view /= bootstrapSubmitId
|
||||||
|
<label for=#{fvId view}>#{fvLabel view}
|
||||||
|
^{fvInput view}
|
||||||
|
^{helpWidget view}
|
||||||
|
$of BootstrapInlineForm
|
||||||
|
$if fvId view /= bootstrapSubmitId
|
||||||
|
<label .sr-only for=#{fvId view}>#{fvLabel view}
|
||||||
|
^{fvInput view}
|
||||||
|
^{helpWidget view}
|
||||||
|
$of BootstrapHorizontalForm labelOffset labelSize inputOffset inputSize
|
||||||
|
$if fvId view /= bootstrapSubmitId
|
||||||
|
<label .control-label .#{toOffset labelOffset} .#{toColumn labelSize} for=#{fvId view}>#{fvLabel view}
|
||||||
|
<div .#{toOffset inputOffset} .#{toColumn inputSize}>
|
||||||
|
^{fvInput view}
|
||||||
|
^{helpWidget view}
|
||||||
|
$else
|
||||||
|
<div .#{toOffset (addGO inputOffset (addGO labelOffset labelSize))} .#{toColumn inputSize}>
|
||||||
|
^{fvInput view}
|
||||||
|
^{helpWidget view}
|
||||||
|
|]
|
||||||
|
return (res, widget)
|
||||||
|
|
||||||
|
|
||||||
|
-- | (Internal) Render a help widget for tooltips and errors.
|
||||||
|
helpWidget :: FieldView site -> WidgetT site IO ()
|
||||||
|
helpWidget view = [whamlet|
|
||||||
|
$maybe tt <- fvTooltip view
|
||||||
|
<span .help-block>#{tt}
|
||||||
|
$maybe err <- fvErrors view
|
||||||
|
<span .help-block>#{err}
|
||||||
|
|]
|
||||||
|
|
||||||
|
|
||||||
|
-- | How the 'bootstrapSubmit' button should be rendered.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
data BootstrapSubmit msg =
|
||||||
|
BootstrapSubmit
|
||||||
|
{ bsValue :: msg
|
||||||
|
-- ^ The text of the submit button.
|
||||||
|
, bsClasses :: Text
|
||||||
|
-- ^ Classes added to the @<button>@.
|
||||||
|
, bsAttrs :: [(Text, Text)]
|
||||||
|
-- ^ Attributes added to the @<button>@.
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance IsString msg => IsString (BootstrapSubmit msg) where
|
||||||
|
fromString msg = BootstrapSubmit (fromString msg) " btn-default " []
|
||||||
|
|
||||||
|
|
||||||
|
-- | A Bootstrap v3 submit button disguised as a field for
|
||||||
|
-- convenience. For example, if your form currently is:
|
||||||
|
--
|
||||||
|
-- > Person <$> areq textField "Name" Nothing
|
||||||
|
-- > <*> areq textField "Surname" Nothing
|
||||||
|
--
|
||||||
|
-- Then just change it to:
|
||||||
|
--
|
||||||
|
-- > Person <$> areq textField "Name" Nothing
|
||||||
|
-- > <*> areq textField "Surname" Nothing
|
||||||
|
-- > <* bootstrapSubmit "Register"
|
||||||
|
--
|
||||||
|
-- (Note that @<*@ is not a typo.)
|
||||||
|
--
|
||||||
|
-- Alternatively, you may also just create the submit button
|
||||||
|
-- manually as well in order to have more control over its
|
||||||
|
-- layout.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
bootstrapSubmit
|
||||||
|
:: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)
|
||||||
|
=> BootstrapSubmit msg -> AForm m ()
|
||||||
|
bootstrapSubmit = formToAForm . liftM (second return) . mbootstrapSubmit
|
||||||
|
|
||||||
|
|
||||||
|
-- | Same as 'bootstrapSubmit' but for monadic forms. This isn't
|
||||||
|
-- as useful since you're not going to use 'renderBootstrap3'
|
||||||
|
-- anyway.
|
||||||
|
--
|
||||||
|
-- Since: yesod-form 1.3.8
|
||||||
|
mbootstrapSubmit
|
||||||
|
:: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)
|
||||||
|
=> BootstrapSubmit msg -> MForm m (FormResult (), FieldView site)
|
||||||
|
mbootstrapSubmit (BootstrapSubmit msg classes attrs) =
|
||||||
|
let res = FormSuccess ()
|
||||||
|
widget = [whamlet|<button class="btn #{classes}" type=submit *{attrs}>_{msg}|]
|
||||||
|
fv = FieldView { fvLabel = ""
|
||||||
|
, fvTooltip = Nothing
|
||||||
|
, fvId = bootstrapSubmitId
|
||||||
|
, fvInput = widget
|
||||||
|
, fvErrors = Nothing
|
||||||
|
, fvRequired = False }
|
||||||
|
in return (res, fv)
|
||||||
|
|
||||||
|
|
||||||
|
-- | A royal hack. Magic id used to identify whether a field
|
||||||
|
-- should have no label. A valid HTML4 id which is probably not
|
||||||
|
-- going to clash with any other id should someone use
|
||||||
|
-- 'bootstrapSubmit' outside 'renderBootstrap3'.
|
||||||
|
bootstrapSubmitId :: Text
|
||||||
|
bootstrapSubmitId = "b:ootstrap___unique__:::::::::::::::::submit-id"
|
||||||
@ -18,6 +18,7 @@ module Yesod.Form.Fields
|
|||||||
, timeField
|
, timeField
|
||||||
, htmlField
|
, htmlField
|
||||||
, emailField
|
, emailField
|
||||||
|
, multiEmailField
|
||||||
, searchField
|
, searchField
|
||||||
, AutoFocus
|
, AutoFocus
|
||||||
, urlField
|
, urlField
|
||||||
@ -36,12 +37,15 @@ module Yesod.Form.Fields
|
|||||||
, selectFieldList
|
, selectFieldList
|
||||||
, radioField
|
, radioField
|
||||||
, radioFieldList
|
, radioFieldList
|
||||||
|
, checkboxesFieldList
|
||||||
|
, checkboxesField
|
||||||
, multiSelectField
|
, multiSelectField
|
||||||
, multiSelectFieldList
|
, multiSelectFieldList
|
||||||
, Option (..)
|
, Option (..)
|
||||||
, OptionList (..)
|
, OptionList (..)
|
||||||
, mkOptionList
|
, mkOptionList
|
||||||
, optionsPersist
|
, optionsPersist
|
||||||
|
, optionsPersistKey
|
||||||
, optionsPairs
|
, optionsPairs
|
||||||
, optionsEnum
|
, optionsEnum
|
||||||
) where
|
) where
|
||||||
@ -61,10 +65,11 @@ import qualified Text.Email.Validate as Email
|
|||||||
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
||||||
import Data.Text.Encoding.Error (lenientDecode)
|
import Data.Text.Encoding.Error (lenientDecode)
|
||||||
import Network.URI (parseURI)
|
import Network.URI (parseURI)
|
||||||
import Database.Persist.Sql (PersistField, PersistFieldSql)
|
import Database.Persist.Sql (PersistField, PersistFieldSql (..))
|
||||||
import Database.Persist (Entity (..))
|
import Database.Persist (Entity (..), SqlType (SqlString))
|
||||||
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
||||||
import Control.Monad (when, unless)
|
import Control.Monad (when, unless)
|
||||||
|
import Data.Either (partitionEithers)
|
||||||
import Data.Maybe (listToMaybe, fromMaybe)
|
import Data.Maybe (listToMaybe, fromMaybe)
|
||||||
|
|
||||||
import qualified Blaze.ByteString.Builder.Html.Utf8 as B
|
import qualified Blaze.ByteString.Builder.Html.Utf8 as B
|
||||||
@ -75,17 +80,22 @@ import Database.Persist (PersistMonadBackend, PersistEntityBackend)
|
|||||||
import Text.Blaze.Html.Renderer.String (renderHtml)
|
import Text.Blaze.Html.Renderer.String (renderHtml)
|
||||||
import qualified Data.ByteString as S
|
import qualified Data.ByteString as S
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import Data.Text (Text, unpack, pack)
|
import Data.Text as T ( Text, append, concat, cons, head
|
||||||
|
, intercalate, isPrefixOf, null, unpack, pack, splitOn
|
||||||
|
)
|
||||||
|
import qualified Data.Text as T (drop, dropWhile)
|
||||||
import qualified Data.Text.Read
|
import qualified Data.Text.Read
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Yesod.Persist (selectList, runDB, Filter, SelectOpt, Key, YesodPersist, PersistEntity, PersistQuery, YesodDB)
|
import Yesod.Persist (selectList, runDB, Filter, SelectOpt, Key, YesodPersist, PersistEntity, PersistQuery)
|
||||||
import Control.Arrow ((&&&))
|
import Control.Arrow ((&&&))
|
||||||
|
|
||||||
import Control.Applicative ((<$>), (<|>))
|
import Control.Applicative ((<$>), (<|>))
|
||||||
|
|
||||||
import Data.Attoparsec.Text (Parser, char, string, digit, skipSpace, endOfInput, parseOnly)
|
import Data.Attoparsec.Text (Parser, char, string, digit, skipSpace, endOfInput, parseOnly)
|
||||||
|
|
||||||
|
import Yesod.Persist.Core
|
||||||
|
|
||||||
defaultFormMessage :: FormMessage -> Text
|
defaultFormMessage :: FormMessage -> Text
|
||||||
defaultFormMessage = englishFormMessage
|
defaultFormMessage = englishFormMessage
|
||||||
|
|
||||||
@ -99,7 +109,7 @@ intField = Field
|
|||||||
|
|
||||||
, fieldView = \theId name attrs val isReq -> toWidget [hamlet|
|
, fieldView = \theId name attrs val isReq -> toWidget [hamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<input id="#{theId}" name="#{name}" *{attrs} type="number" :isReq:required="" value="#{showVal val}">
|
<input id="#{theId}" name="#{name}" *{attrs} type="number" step=1 :isReq:required="" value="#{showVal val}">
|
||||||
|]
|
|]
|
||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
@ -110,13 +120,13 @@ $newline never
|
|||||||
doubleField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Double
|
doubleField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Double
|
||||||
doubleField = Field
|
doubleField = Field
|
||||||
{ fieldParse = parseHelper $ \s ->
|
{ fieldParse = parseHelper $ \s ->
|
||||||
case Data.Text.Read.double s of
|
case Data.Text.Read.double (prependZero s) of
|
||||||
Right (a, "") -> Right a
|
Right (a, "") -> Right a
|
||||||
_ -> Left $ MsgInvalidNumber s
|
_ -> Left $ MsgInvalidNumber s
|
||||||
|
|
||||||
, fieldView = \theId name attrs val isReq -> toWidget [hamlet|
|
, fieldView = \theId name attrs val isReq -> toWidget [hamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required="" value="#{showVal val}">
|
<input id="#{theId}" name="#{name}" *{attrs} type="number" step=any :isReq:required="" value="#{showVal val}">
|
||||||
|]
|
|]
|
||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
@ -163,7 +173,9 @@ $newline never
|
|||||||
-- | A newtype wrapper around a 'Text' that converts newlines to HTML
|
-- | A newtype wrapper around a 'Text' that converts newlines to HTML
|
||||||
-- br-tags.
|
-- br-tags.
|
||||||
newtype Textarea = Textarea { unTextarea :: Text }
|
newtype Textarea = Textarea { unTextarea :: Text }
|
||||||
deriving (Show, Read, Eq, PersistField, PersistFieldSql, Ord)
|
deriving (Show, Read, Eq, PersistField, Ord, ToJSON, FromJSON)
|
||||||
|
instance PersistFieldSql Textarea where
|
||||||
|
sqlType _ = SqlString
|
||||||
instance ToHtml Textarea where
|
instance ToHtml Textarea where
|
||||||
toHtml =
|
toHtml =
|
||||||
unsafeByteString
|
unsafeByteString
|
||||||
@ -295,12 +307,37 @@ $newline never
|
|||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- |
|
||||||
|
--
|
||||||
|
-- Since 1.3.7
|
||||||
|
multiEmailField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m [Text]
|
||||||
|
multiEmailField = Field
|
||||||
|
{ fieldParse = parseHelper $
|
||||||
|
\s ->
|
||||||
|
let addrs = map validate $ splitOn "," s
|
||||||
|
in case partitionEithers addrs of
|
||||||
|
([], good) -> Right good
|
||||||
|
(bad, _) -> Left $ MsgInvalidEmail $ cat bad
|
||||||
|
, fieldView = \theId name attrs val isReq -> toWidget [hamlet|
|
||||||
|
$newline never
|
||||||
|
<input id="#{theId}" name="#{name}" *{attrs} type="email" multiple :isReq:required="" value="#{either id cat val}">
|
||||||
|
|]
|
||||||
|
, fieldEnctype = UrlEncoded
|
||||||
|
}
|
||||||
|
where
|
||||||
|
-- report offending address along with error
|
||||||
|
validate a = case Email.validate $ encodeUtf8 a of
|
||||||
|
Left e -> Left $ T.concat [a, " (", pack e, ")"]
|
||||||
|
Right r -> Right $ emailToText r
|
||||||
|
cat = intercalate ", "
|
||||||
|
emailToText = decodeUtf8With lenientDecode . Email.toByteString
|
||||||
|
|
||||||
type AutoFocus = Bool
|
type AutoFocus = Bool
|
||||||
searchField :: Monad m => RenderMessage (HandlerSite m) FormMessage => AutoFocus -> Field m Text
|
searchField :: Monad m => RenderMessage (HandlerSite m) FormMessage => AutoFocus -> Field m Text
|
||||||
searchField autoFocus = Field
|
searchField autoFocus = Field
|
||||||
{ fieldParse = parseHelper Right
|
{ fieldParse = parseHelper Right
|
||||||
, fieldView = \theId name attrs val isReq -> do
|
, fieldView = \theId name attrs val isReq -> do
|
||||||
[whamlet|\
|
[whamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<input id="#{theId}" name="#{name}" *{attrs} type="search" :isReq:required="" :autoFocus:autofocus="" value="#{either id id val}">
|
<input id="#{theId}" name="#{name}" *{attrs} type="search" :isReq:required="" :autoFocus:autofocus="" value="#{either id id val}">
|
||||||
|]
|
|]
|
||||||
@ -385,6 +422,28 @@ radioFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg)
|
|||||||
-> Field (HandlerT site IO) a
|
-> Field (HandlerT site IO) a
|
||||||
radioFieldList = radioField . optionsPairs
|
radioFieldList = radioField . optionsPairs
|
||||||
|
|
||||||
|
checkboxesFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg) => [(msg, a)]
|
||||||
|
-> Field (HandlerT site IO) [a]
|
||||||
|
checkboxesFieldList = checkboxesField . optionsPairs
|
||||||
|
|
||||||
|
checkboxesField :: (Eq a, RenderMessage site FormMessage)
|
||||||
|
=> HandlerT site IO (OptionList a)
|
||||||
|
-> Field (HandlerT site IO) [a]
|
||||||
|
checkboxesField ioptlist = (multiSelectField ioptlist)
|
||||||
|
{ fieldView =
|
||||||
|
\theId name attrs val isReq -> do
|
||||||
|
opts <- fmap olOptions $ handlerToWidget ioptlist
|
||||||
|
let optselected (Left _) _ = False
|
||||||
|
optselected (Right vals) opt = (optionInternalValue opt) `elem` vals
|
||||||
|
[whamlet|
|
||||||
|
<span ##{theId}>
|
||||||
|
$forall opt <- opts
|
||||||
|
<label>
|
||||||
|
<input type=checkbox name=#{name} value=#{optionExternalValue opt} *{attrs} :optselected val opt:checked>
|
||||||
|
#{optionDisplay opt}
|
||||||
|
|]
|
||||||
|
}
|
||||||
|
|
||||||
radioField :: (Eq a, RenderMessage site FormMessage)
|
radioField :: (Eq a, RenderMessage site FormMessage)
|
||||||
=> HandlerT site IO (OptionList a)
|
=> HandlerT site IO (OptionList a)
|
||||||
-> Field (HandlerT site IO) a
|
-> Field (HandlerT site IO) a
|
||||||
@ -434,6 +493,8 @@ $newline never
|
|||||||
"yes" -> Right $ Just True
|
"yes" -> Right $ Just True
|
||||||
"on" -> Right $ Just True
|
"on" -> Right $ Just True
|
||||||
"no" -> Right $ Just False
|
"no" -> Right $ Just False
|
||||||
|
"true" -> Right $ Just True
|
||||||
|
"false" -> Right $ Just False
|
||||||
t -> Left $ SomeMessage $ MsgInvalidBool t
|
t -> Left $ SomeMessage $ MsgInvalidBool t
|
||||||
showVal = either (\_ -> False)
|
showVal = either (\_ -> False)
|
||||||
|
|
||||||
@ -495,9 +556,9 @@ optionsEnum :: (MonadHandler m, Show a, Enum a, Bounded a) => m (OptionList a)
|
|||||||
optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound]
|
optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound]
|
||||||
|
|
||||||
optionsPersist :: ( YesodPersist site, PersistEntity a
|
optionsPersist :: ( YesodPersist site, PersistEntity a
|
||||||
, PersistQuery (YesodDB site)
|
, PersistQuery (YesodPersistBackend site (HandlerT site IO))
|
||||||
, PathPiece (Key a)
|
, PathPiece (Key a)
|
||||||
, PersistEntityBackend a ~ PersistMonadBackend (YesodDB site)
|
, PersistEntityBackend a ~ PersistMonadBackend (YesodPersistBackend site (HandlerT site IO))
|
||||||
, RenderMessage site msg
|
, RenderMessage site msg
|
||||||
)
|
)
|
||||||
=> [Filter a]
|
=> [Filter a]
|
||||||
@ -513,6 +574,31 @@ optionsPersist filts ords toDisplay = fmap mkOptionList $ do
|
|||||||
, optionExternalValue = toPathPiece key
|
, optionExternalValue = toPathPiece key
|
||||||
}) pairs
|
}) pairs
|
||||||
|
|
||||||
|
-- | An alternative to 'optionsPersist' which returns just the @Key@ instead of
|
||||||
|
-- the entire @Entity@.
|
||||||
|
--
|
||||||
|
-- Since 1.3.2
|
||||||
|
optionsPersistKey
|
||||||
|
:: (YesodPersist site
|
||||||
|
, PersistEntity a
|
||||||
|
, PersistQuery (YesodPersistBackend site (HandlerT site IO))
|
||||||
|
, PathPiece (Key a)
|
||||||
|
, RenderMessage site msg
|
||||||
|
, PersistEntityBackend a ~ PersistMonadBackend (YesodPersistBackend site (HandlerT site IO)))
|
||||||
|
=> [Filter a]
|
||||||
|
-> [SelectOpt a]
|
||||||
|
-> (a -> msg)
|
||||||
|
-> HandlerT site IO (OptionList (Key a))
|
||||||
|
|
||||||
|
optionsPersistKey filts ords toDisplay = fmap mkOptionList $ do
|
||||||
|
mr <- getMessageRender
|
||||||
|
pairs <- runDB $ selectList filts ords
|
||||||
|
return $ map (\(Entity key value) -> Option
|
||||||
|
{ optionDisplay = mr (toDisplay value)
|
||||||
|
, optionInternalValue = key
|
||||||
|
, optionExternalValue = toPathPiece key
|
||||||
|
}) pairs
|
||||||
|
|
||||||
selectFieldHelper
|
selectFieldHelper
|
||||||
:: (Eq a, RenderMessage site FormMessage)
|
:: (Eq a, RenderMessage site FormMessage)
|
||||||
=> (Text -> Text -> [(Text, Text)] -> WidgetT site IO () -> WidgetT site IO ())
|
=> (Text -> Text -> [(Text, Text)] -> WidgetT site IO () -> WidgetT site IO ())
|
||||||
@ -531,7 +617,7 @@ selectFieldHelper outside onOpt inside opts' = Field
|
|||||||
flip mapM_ opts $ \opt -> inside
|
flip mapM_ opts $ \opt -> inside
|
||||||
theId
|
theId
|
||||||
name
|
name
|
||||||
attrs
|
((if isReq then (("required", "required"):) else id) attrs)
|
||||||
(optionExternalValue opt)
|
(optionExternalValue opt)
|
||||||
((render opts val) == optionExternalValue opt)
|
((render opts val) == optionExternalValue opt)
|
||||||
(optionDisplay opt)
|
(optionDisplay opt)
|
||||||
@ -628,3 +714,19 @@ $newline never
|
|||||||
incrInts :: Ints -> Ints
|
incrInts :: Ints -> Ints
|
||||||
incrInts (IntSingle i) = IntSingle $ i + 1
|
incrInts (IntSingle i) = IntSingle $ i + 1
|
||||||
incrInts (IntCons i is) = (i + 1) `IntCons` is
|
incrInts (IntCons i is) = (i + 1) `IntCons` is
|
||||||
|
|
||||||
|
|
||||||
|
-- | Adds a '0' to some text so that it may be recognized as a double.
|
||||||
|
-- The read ftn does not recognize ".3" as 0.3 nor "-.3" as -0.3, so this
|
||||||
|
-- function changes ".xxx" to "0.xxx" and "-.xxx" to "-0.xxx"
|
||||||
|
|
||||||
|
prependZero :: Text -> Text
|
||||||
|
prependZero t0 = if T.null t1
|
||||||
|
then t1
|
||||||
|
else if T.head t1 == '.'
|
||||||
|
then '0' `T.cons` t1
|
||||||
|
else if "-." `T.isPrefixOf` t1
|
||||||
|
then "-0." `T.append` (T.drop 2 t1)
|
||||||
|
else t1
|
||||||
|
|
||||||
|
where t1 = T.dropWhile ((==) ' ') t0
|
||||||
|
|||||||
@ -23,7 +23,10 @@ module Yesod.Form.Functions
|
|||||||
, runFormGet
|
, runFormGet
|
||||||
-- * Generate a blank form
|
-- * Generate a blank form
|
||||||
, generateFormPost
|
, generateFormPost
|
||||||
|
, generateFormGet'
|
||||||
, generateFormGet
|
, generateFormGet
|
||||||
|
-- * More than one form on a handler
|
||||||
|
, identifyForm
|
||||||
-- * Rendering
|
-- * Rendering
|
||||||
, FormRender
|
, FormRender
|
||||||
, renderTable
|
, renderTable
|
||||||
@ -39,15 +42,16 @@ module Yesod.Form.Functions
|
|||||||
-- * Utilities
|
-- * Utilities
|
||||||
, fieldSettingsLabel
|
, fieldSettingsLabel
|
||||||
, parseHelper
|
, parseHelper
|
||||||
|
, parseHelperGen
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Yesod.Form.Types
|
import Yesod.Form.Types
|
||||||
import Data.Text (Text, pack)
|
import Data.Text (Text, pack)
|
||||||
import Control.Arrow (second)
|
import Control.Arrow (second)
|
||||||
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST)
|
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local)
|
||||||
import Control.Monad.Trans.Class
|
import Control.Monad.Trans.Class
|
||||||
import Control.Monad (liftM, join)
|
import Control.Monad (liftM, join)
|
||||||
import Crypto.Classes (constTimeEq)
|
import Data.Byteable (constEqBytes)
|
||||||
import Text.Blaze (Markup, toMarkup)
|
import Text.Blaze (Markup, toMarkup)
|
||||||
#define Html Markup
|
#define Html Markup
|
||||||
#define toHtml toMarkup
|
#define toHtml toMarkup
|
||||||
@ -99,13 +103,18 @@ askFiles = do
|
|||||||
(x, _, _) <- ask
|
(x, _, _) <- ask
|
||||||
return $ liftM snd x
|
return $ liftM snd x
|
||||||
|
|
||||||
|
-- | Converts a form field into monadic form. This field requires a value
|
||||||
|
-- and will return 'FormFailure' if left empty.
|
||||||
mreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
mreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
=> Field m a
|
=> Field m a -- ^ form field
|
||||||
-> FieldSettings site
|
-> FieldSettings site -- ^ settings for this field
|
||||||
-> Maybe a
|
-> Maybe a -- ^ optional default value
|
||||||
-> MForm m (FormResult a, FieldView site)
|
-> MForm m (FormResult a, FieldView site)
|
||||||
mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
|
mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
|
||||||
|
|
||||||
|
-- | Converts a form field into monadic form. This field is optional, i.e.
|
||||||
|
-- if filled in, it returns 'Just a', if left empty, it returns 'Nothing'.
|
||||||
|
-- Arguments are the same as for 'mreq' (apart from type of default value).
|
||||||
mopt :: (site ~ HandlerSite m, MonadHandler m)
|
mopt :: (site ~ HandlerSite m, MonadHandler m)
|
||||||
=> Field m a
|
=> Field m a
|
||||||
-> FieldSettings site
|
-> FieldSettings site
|
||||||
@ -155,6 +164,7 @@ mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
|
|||||||
, fvRequired = isReq
|
, fvRequired = isReq
|
||||||
})
|
})
|
||||||
|
|
||||||
|
-- | Applicative equivalent of 'mreq'.
|
||||||
areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
=> Field m a
|
=> Field m a
|
||||||
-> FieldSettings site
|
-> FieldSettings site
|
||||||
@ -162,6 +172,7 @@ areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
|||||||
-> AForm m a
|
-> AForm m a
|
||||||
areq a b = formToAForm . liftM (second return) . mreq a b
|
areq a b = formToAForm . liftM (second return) . mreq a b
|
||||||
|
|
||||||
|
-- | Applicative equivalent of 'mopt'.
|
||||||
aopt :: MonadHandler m
|
aopt :: MonadHandler m
|
||||||
=> Field m a
|
=> Field m a
|
||||||
-> FieldSettings (HandlerSite m)
|
-> FieldSettings (HandlerSite m)
|
||||||
@ -175,7 +186,7 @@ runFormGeneric :: Monad m
|
|||||||
-> [Text]
|
-> [Text]
|
||||||
-> Maybe (Env, FileEnv)
|
-> Maybe (Env, FileEnv)
|
||||||
-> m (a, Enctype)
|
-> m (a, Enctype)
|
||||||
runFormGeneric form site langs env = evalRWST form (env, site, langs) (IntSingle 1)
|
runFormGeneric form site langs env = evalRWST form (env, site, langs) (IntSingle 0)
|
||||||
|
|
||||||
-- | This function is used to both initially render a form and to later extract
|
-- | This function is used to both initially render a form and to later extract
|
||||||
-- results from it. Note that, due to CSRF protection and a few other issues,
|
-- results from it. Note that, due to CSRF protection and a few other issues,
|
||||||
@ -213,12 +224,12 @@ postHelper form env = do
|
|||||||
| not (Map.lookup tokenKey params === reqToken req) ->
|
| not (Map.lookup tokenKey params === reqToken req) ->
|
||||||
FormFailure [renderMessage m langs MsgCsrfWarning]
|
FormFailure [renderMessage m langs MsgCsrfWarning]
|
||||||
_ -> res
|
_ -> res
|
||||||
where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constTimeEq` TE.encodeUtf8 t2
|
where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constEqBytes` TE.encodeUtf8 t2
|
||||||
Nothing === Nothing = True -- It's important to use constTimeEq
|
Nothing === Nothing = True -- It's important to use constTimeEq
|
||||||
_ === _ = False -- in order to avoid timing attacks.
|
_ === _ = False -- in order to avoid timing attacks.
|
||||||
return ((res', xml), enctype)
|
return ((res', xml), enctype)
|
||||||
|
|
||||||
-- | Similar to 'runFormPost', except it always ignore the currently available
|
-- | Similar to 'runFormPost', except it always ignores the currently available
|
||||||
-- environment. This is necessary in cases like a wizard UI, where a single
|
-- environment. This is necessary in cases like a wizard UI, where a single
|
||||||
-- page will both receive and incoming form and produce a new, blank form. For
|
-- page will both receive and incoming form and produce a new, blank form. For
|
||||||
-- general usage, you can stick with @runFormPost@.
|
-- general usage, you can stick with @runFormPost@.
|
||||||
@ -259,6 +270,17 @@ runFormGet form = do
|
|||||||
Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty)
|
Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty)
|
||||||
getHelper form env
|
getHelper form env
|
||||||
|
|
||||||
|
{- FIXME: generateFormGet' "Will be renamed to generateFormGet in next verison of Yesod" -}
|
||||||
|
-- |
|
||||||
|
--
|
||||||
|
-- Since 1.3.11
|
||||||
|
generateFormGet'
|
||||||
|
:: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)
|
||||||
|
=> (Html -> MForm m (FormResult a, xml))
|
||||||
|
-> m (xml, Enctype)
|
||||||
|
generateFormGet' form = first snd `liftM` getHelper form Nothing
|
||||||
|
|
||||||
|
{-# DEPRECATED generateFormGet "Will require RenderMessage in next verison of Yesod" #-}
|
||||||
generateFormGet :: MonadHandler m
|
generateFormGet :: MonadHandler m
|
||||||
=> (Html -> MForm m a)
|
=> (Html -> MForm m a)
|
||||||
-> m (a, Enctype)
|
-> m (a, Enctype)
|
||||||
@ -277,6 +299,57 @@ getHelper form env = do
|
|||||||
m <- getYesod
|
m <- getYesod
|
||||||
runFormGeneric (form fragment) m langs env
|
runFormGeneric (form fragment) m langs env
|
||||||
|
|
||||||
|
|
||||||
|
-- | Creates a hidden field on the form that identifies it. This
|
||||||
|
-- identification is then used to distinguish between /missing/
|
||||||
|
-- and /wrong/ form data when a single handler contains more than
|
||||||
|
-- one form.
|
||||||
|
--
|
||||||
|
-- For instance, if you have the following code on your handler:
|
||||||
|
--
|
||||||
|
-- > ((fooRes, fooWidget), fooEnctype) <- runFormPost fooForm
|
||||||
|
-- > ((barRes, barWidget), barEnctype) <- runFormPost barForm
|
||||||
|
--
|
||||||
|
-- Then replace it with
|
||||||
|
--
|
||||||
|
-- > ((fooRes, fooWidget), fooEnctype) <- runFormPost $ identifyForm "foo" fooForm
|
||||||
|
-- > ((barRes, barWidget), barEnctype) <- runFormPost $ identifyForm "bar" barForm
|
||||||
|
--
|
||||||
|
-- Note that it's your responsibility to ensure that the
|
||||||
|
-- identification strings are unique (using the same one twice on a
|
||||||
|
-- single handler will not generate any errors). This allows you
|
||||||
|
-- to create a variable number of forms and still have them work
|
||||||
|
-- even if their number or order change between the HTML
|
||||||
|
-- generation and the form submission.
|
||||||
|
identifyForm
|
||||||
|
:: Monad m
|
||||||
|
=> Text -- ^ Form identification string.
|
||||||
|
-> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
|
||||||
|
-> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
|
||||||
|
identifyForm identVal form = \fragment -> do
|
||||||
|
-- Create hidden <input>.
|
||||||
|
let fragment' =
|
||||||
|
[shamlet|
|
||||||
|
<input type=hidden name=#{identifyFormKey} value=#{identVal}>
|
||||||
|
#{fragment}
|
||||||
|
|]
|
||||||
|
|
||||||
|
-- Check if we got its value back.
|
||||||
|
mp <- askParams
|
||||||
|
let missing = (mp >>= Map.lookup identifyFormKey) /= Just [identVal]
|
||||||
|
|
||||||
|
-- Run the form proper (with our hidden <input>). If the
|
||||||
|
-- data is missing, then do not provide any params to the
|
||||||
|
-- form, which will turn its result into FormMissing. Also,
|
||||||
|
-- doing this avoids having lots of fields with red errors.
|
||||||
|
let eraseParams | missing = local (\(_, h, l) -> (Nothing, h, l))
|
||||||
|
| otherwise = id
|
||||||
|
eraseParams (form fragment')
|
||||||
|
|
||||||
|
identifyFormKey :: Text
|
||||||
|
identifyFormKey = "_formid"
|
||||||
|
|
||||||
|
|
||||||
type FormRender m a =
|
type FormRender m a =
|
||||||
AForm m a
|
AForm m a
|
||||||
-> Html
|
-> Html
|
||||||
@ -326,7 +399,9 @@ $forall view <- views
|
|||||||
|]
|
|]
|
||||||
return (res, widget)
|
return (res, widget)
|
||||||
|
|
||||||
-- | Render a form using Bootstrap-friendly shamlet syntax.
|
-- | Render a form using Bootstrap v2-friendly shamlet syntax.
|
||||||
|
-- If you're using Bootstrap v3, then you should use the
|
||||||
|
-- functions from module "Yesod.Form.Bootstrap3".
|
||||||
--
|
--
|
||||||
-- Sample Hamlet:
|
-- Sample Hamlet:
|
||||||
--
|
--
|
||||||
@ -361,6 +436,7 @@ renderBootstrap aform fragment = do
|
|||||||
<span .help-block>#{err}
|
<span .help-block>#{err}
|
||||||
|]
|
|]
|
||||||
return (res, widget)
|
return (res, widget)
|
||||||
|
{-# DEPRECATED renderBootstrap "Please use the Yesod.Form.Bootstrap3 module." #-}
|
||||||
|
|
||||||
check :: (Monad m, RenderMessage (HandlerSite m) msg)
|
check :: (Monad m, RenderMessage (HandlerSite m) msg)
|
||||||
=> (a -> Either msg a)
|
=> (a -> Either msg a)
|
||||||
@ -421,6 +497,15 @@ fieldSettingsLabel msg = FieldSettings (SomeMessage msg) Nothing Nothing Nothing
|
|||||||
parseHelper :: (Monad m, RenderMessage site FormMessage)
|
parseHelper :: (Monad m, RenderMessage site FormMessage)
|
||||||
=> (Text -> Either FormMessage a)
|
=> (Text -> Either FormMessage a)
|
||||||
-> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
|
-> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
|
||||||
parseHelper _ [] _ = return $ Right Nothing
|
parseHelper = parseHelperGen
|
||||||
parseHelper _ ("":_) _ = return $ Right Nothing
|
|
||||||
parseHelper f (x:_) _ = return $ either (Left . SomeMessage) (Right . Just) $ f x
|
-- | A generalized version of 'parseHelper', allowing any type for the message
|
||||||
|
-- indicating a bad parse.
|
||||||
|
--
|
||||||
|
-- Since 1.3.6
|
||||||
|
parseHelperGen :: (Monad m, RenderMessage site msg)
|
||||||
|
=> (Text -> Either msg a)
|
||||||
|
-> [Text] -> [FileInfo] -> m (Either (SomeMessage site) (Maybe a))
|
||||||
|
parseHelperGen _ [] _ = return $ Right Nothing
|
||||||
|
parseHelperGen _ ("":_) _ = return $ Right Nothing
|
||||||
|
parseHelperGen f (x:_) _ = return $ either (Left . SomeMessage) (Right . Just) $ f x
|
||||||
|
|||||||
@ -4,6 +4,7 @@ module Yesod.Form.Input
|
|||||||
( FormInput (..)
|
( FormInput (..)
|
||||||
, runInputGet
|
, runInputGet
|
||||||
, runInputPost
|
, runInputPost
|
||||||
|
, runInputPostResult
|
||||||
, ireq
|
, ireq
|
||||||
, iopt
|
, iopt
|
||||||
) where
|
) where
|
||||||
@ -66,11 +67,22 @@ toMap :: [(Text, a)] -> Map.Map Text [a]
|
|||||||
toMap = Map.unionsWith (++) . map (\(x, y) -> Map.singleton x [y])
|
toMap = Map.unionsWith (++) . map (\(x, y) -> Map.singleton x [y])
|
||||||
|
|
||||||
runInputPost :: MonadHandler m => FormInput m a -> m a
|
runInputPost :: MonadHandler m => FormInput m a -> m a
|
||||||
runInputPost (FormInput f) = do
|
runInputPost fi = do
|
||||||
|
emx <- runInputPostHelper fi
|
||||||
|
case emx of
|
||||||
|
Left errs -> invalidArgs errs
|
||||||
|
Right x -> return x
|
||||||
|
|
||||||
|
runInputPostResult :: MonadHandler m => FormInput m a -> m (FormResult a)
|
||||||
|
runInputPostResult fi = do
|
||||||
|
emx <- runInputPostHelper fi
|
||||||
|
case emx of
|
||||||
|
Left errs -> return $ FormFailure errs
|
||||||
|
Right x -> return $ FormSuccess x
|
||||||
|
|
||||||
|
runInputPostHelper :: MonadHandler m => FormInput m a -> m (Either [Text] a)
|
||||||
|
runInputPostHelper (FormInput f) = do
|
||||||
(env, fenv) <- liftM (toMap *** toMap) runRequestBody
|
(env, fenv) <- liftM (toMap *** toMap) runRequestBody
|
||||||
m <- getYesod
|
m <- getYesod
|
||||||
l <- languages
|
l <- languages
|
||||||
emx <- f m l env fenv
|
fmap (either (Left . ($ [])) Right) $ f m l env fenv
|
||||||
case emx of
|
|
||||||
Left errs -> invalidArgs $ errs []
|
|
||||||
Right x -> return x
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ module Yesod.Form.MassInput
|
|||||||
|
|
||||||
import Yesod.Form.Types
|
import Yesod.Form.Types
|
||||||
import Yesod.Form.Functions
|
import Yesod.Form.Functions
|
||||||
import Yesod.Form.Fields (boolField)
|
import Yesod.Form.Fields (checkBoxField)
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Control.Monad.Trans.RWS (get, put, ask)
|
import Control.Monad.Trans.RWS (get, put, ask)
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
@ -97,7 +97,7 @@ $newline never
|
|||||||
<input type=hidden name=#{deleteName} value=yes>
|
<input type=hidden name=#{deleteName} value=yes>
|
||||||
|]
|
|]
|
||||||
_ -> do
|
_ -> do
|
||||||
(_, xml2) <- aFormToForm $ areq boolField FieldSettings
|
(_, xml2) <- aFormToForm $ areq checkBoxField FieldSettings
|
||||||
{ fsLabel = SomeMessage MsgDelete
|
{ fsLabel = SomeMessage MsgDelete
|
||||||
, fsTooltip = Nothing
|
, fsTooltip = Nothing
|
||||||
, fsName = Just deleteName
|
, fsName = Just deleteName
|
||||||
|
|||||||
@ -98,11 +98,11 @@ instance Monad m => Functor (AForm m) where
|
|||||||
where
|
where
|
||||||
go (w, x, y, z) = (fmap f w, x, y, z)
|
go (w, x, y, z) = (fmap f w, x, y, z)
|
||||||
instance Monad m => Applicative (AForm m) where
|
instance Monad m => Applicative (AForm m) where
|
||||||
pure x = AForm $ const $ const $ \ints -> return (FormSuccess x, mempty, ints, mempty)
|
pure x = AForm $ const $ const $ \ints -> return (FormSuccess x, id, ints, mempty)
|
||||||
(AForm f) <*> (AForm g) = AForm $ \mr env ints -> do
|
(AForm f) <*> (AForm g) = AForm $ \mr env ints -> do
|
||||||
(a, b, ints', c) <- f mr env ints
|
(a, b, ints', c) <- f mr env ints
|
||||||
(x, y, ints'', z) <- g mr env ints'
|
(x, y, ints'', z) <- g mr env ints'
|
||||||
return (a <*> x, b `mappend` y, ints'', c `mappend` z)
|
return (a <*> x, b . y, ints'', c `mappend` z)
|
||||||
instance (Monad m, Monoid a) => Monoid (AForm m a) where
|
instance (Monad m, Monoid a) => Monoid (AForm m a) where
|
||||||
mempty = pure mempty
|
mempty = pure mempty
|
||||||
mappend a b = mappend <$> a <*> b
|
mappend a b = mappend <$> a <*> b
|
||||||
|
|||||||
@ -23,7 +23,8 @@ mkYesod "HelloForms" [parseRoutes|
|
|||||||
/file FileR GET POST
|
/file FileR GET POST
|
||||||
|]
|
|]
|
||||||
|
|
||||||
myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,,,)
|
myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,,,,,)
|
||||||
|
<*> pure "pure works!"
|
||||||
<*> areq boolField "Bool field" Nothing
|
<*> areq boolField "Bool field" Nothing
|
||||||
<*> aopt boolField "Opt bool field" Nothing
|
<*> aopt boolField "Opt bool field" Nothing
|
||||||
<*> areq textField "Text field" Nothing
|
<*> areq textField "Text field" Nothing
|
||||||
@ -33,6 +34,7 @@ myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,,,)
|
|||||||
<*> aopt (multiSelectFieldList fruits) "Opt multi select field" Nothing
|
<*> aopt (multiSelectFieldList fruits) "Opt multi select field" Nothing
|
||||||
<*> aopt intField "Opt int field" Nothing
|
<*> aopt intField "Opt int field" Nothing
|
||||||
<*> aopt (radioFieldList fruits) "Opt radio" Nothing
|
<*> aopt (radioFieldList fruits) "Opt radio" Nothing
|
||||||
|
<*> aopt multiEmailField "Opt multi email" Nothing
|
||||||
|
|
||||||
data HelloForms = HelloForms
|
data HelloForms = HelloForms
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-form
|
name: yesod-form
|
||||||
version: 1.3.0.1
|
version: 1.3.11
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -17,10 +17,11 @@ library
|
|||||||
, yesod-core >= 1.2 && < 1.3
|
, yesod-core >= 1.2 && < 1.3
|
||||||
, yesod-persistent >= 1.2 && < 1.3
|
, yesod-persistent >= 1.2 && < 1.3
|
||||||
, time >= 1.1.4
|
, time >= 1.1.4
|
||||||
, hamlet >= 1.1 && < 1.2
|
, hamlet >= 1.1.8
|
||||||
, shakespeare-css >= 1.0 && < 1.1
|
, shakespeare
|
||||||
, shakespeare-js >= 1.0.2 && < 1.2
|
, shakespeare-css >= 1.0
|
||||||
, persistent >= 1.2 && < 1.3
|
, shakespeare-js >= 1.0.2
|
||||||
|
, persistent >= 1.2 && < 1.4
|
||||||
, template-haskell
|
, template-haskell
|
||||||
, transformers >= 0.2.2
|
, transformers >= 0.2.2
|
||||||
, data-default
|
, data-default
|
||||||
@ -35,13 +36,14 @@ library
|
|||||||
, blaze-html >= 0.5
|
, blaze-html >= 0.5
|
||||||
, blaze-markup >= 0.5.1
|
, blaze-markup >= 0.5.1
|
||||||
, attoparsec >= 0.10
|
, attoparsec >= 0.10
|
||||||
, crypto-api >= 0.8
|
, byteable
|
||||||
, aeson
|
, aeson
|
||||||
, resourcet
|
, resourcet
|
||||||
|
|
||||||
exposed-modules: Yesod.Form
|
exposed-modules: Yesod.Form
|
||||||
Yesod.Form.Types
|
Yesod.Form.Types
|
||||||
Yesod.Form.Functions
|
Yesod.Form.Functions
|
||||||
|
Yesod.Form.Bootstrap3
|
||||||
Yesod.Form.Input
|
Yesod.Form.Input
|
||||||
Yesod.Form.Fields
|
Yesod.Form.Fields
|
||||||
Yesod.Form.Jquery
|
Yesod.Form.Jquery
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
-- | Generation of Atom newsfeeds.
|
-- | Generation of Atom newsfeeds.
|
||||||
module Yesod.AtomFeed
|
module Yesod.AtomFeed
|
||||||
( atomFeed
|
( atomFeed
|
||||||
|
, atomFeedText
|
||||||
, atomLink
|
, atomLink
|
||||||
, RepAtom (..)
|
, RepAtom (..)
|
||||||
, module Yesod.FeedTypes
|
, module Yesod.FeedTypes
|
||||||
@ -47,6 +48,11 @@ atomFeed feed = do
|
|||||||
render <- getUrlRender
|
render <- getUrlRender
|
||||||
return $ RepAtom $ toContent $ renderLBS def $ template feed render
|
return $ RepAtom $ toContent $ renderLBS def $ template feed render
|
||||||
|
|
||||||
|
-- | Same as @'atomFeed'@ but for @'Feed Text'@. Useful for cases where you are
|
||||||
|
-- generating a feed of external links.
|
||||||
|
atomFeedText :: MonadHandler m => Feed Text -> m RepAtom
|
||||||
|
atomFeedText feed = return $ RepAtom $ toContent $ renderLBS def $ template feed id
|
||||||
|
|
||||||
template :: Feed url -> (url -> Text) -> Document
|
template :: Feed url -> (url -> Text) -> Document
|
||||||
template Feed {..} render =
|
template Feed {..} render =
|
||||||
Document (Prologue [] Nothing []) (addNS root) []
|
Document (Prologue [] Nothing []) (addNS root) []
|
||||||
@ -62,7 +68,7 @@ template Feed {..} render =
|
|||||||
: Element "link" (Map.singleton "href" $ render feedLinkHome) []
|
: Element "link" (Map.singleton "href" $ render feedLinkHome) []
|
||||||
: Element "updated" Map.empty [NodeContent $ formatW3 feedUpdated]
|
: Element "updated" Map.empty [NodeContent $ formatW3 feedUpdated]
|
||||||
: Element "id" Map.empty [NodeContent $ render feedLinkHome]
|
: Element "id" Map.empty [NodeContent $ render feedLinkHome]
|
||||||
: Element "author" Map.empty [NodeContent feedAuthor]
|
: Element "author" Map.empty [NodeElement $ Element "name" Map.empty [NodeContent feedAuthor]]
|
||||||
: map (flip entryTemplate render) feedEntries
|
: map (flip entryTemplate render) feedEntries
|
||||||
|
|
||||||
entryTemplate :: FeedEntry url -> (url -> Text) -> Element
|
entryTemplate :: FeedEntry url -> (url -> Text) -> Element
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
module Yesod.Feed
|
module Yesod.Feed
|
||||||
( newsFeed
|
( newsFeed
|
||||||
|
, newsFeedText
|
||||||
, module Yesod.FeedTypes
|
, module Yesod.FeedTypes
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -29,3 +30,10 @@ newsFeed :: MonadHandler m => Feed (Route (HandlerSite m)) -> m TypedContent
|
|||||||
newsFeed f = selectRep $ do
|
newsFeed f = selectRep $ do
|
||||||
provideRep $ atomFeed f
|
provideRep $ atomFeed f
|
||||||
provideRep $ rssFeed f
|
provideRep $ rssFeed f
|
||||||
|
|
||||||
|
-- | Same as @'newsFeed'@ but for @'Feed Text'@. Useful for cases where you are
|
||||||
|
-- generating a feed of external links.
|
||||||
|
newsFeedText :: MonadHandler m => Feed Text -> m TypedContent
|
||||||
|
newsFeedText f = selectRep $ do
|
||||||
|
provideRep $ atomFeedText f
|
||||||
|
provideRep $ rssFeedText f
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
module Yesod.RssFeed
|
module Yesod.RssFeed
|
||||||
( rssFeed
|
( rssFeed
|
||||||
|
, rssFeedText
|
||||||
, rssLink
|
, rssLink
|
||||||
, RepRss (..)
|
, RepRss (..)
|
||||||
, module Yesod.FeedTypes
|
, module Yesod.FeedTypes
|
||||||
@ -44,6 +45,11 @@ rssFeed feed = do
|
|||||||
render <- getUrlRender
|
render <- getUrlRender
|
||||||
return $ RepRss $ toContent $ renderLBS def $ template feed render
|
return $ RepRss $ toContent $ renderLBS def $ template feed render
|
||||||
|
|
||||||
|
-- | Same as @'rssFeed'@ but for @'Feed Text'@. Useful for cases where you are
|
||||||
|
-- generating a feed of external links.
|
||||||
|
rssFeedText :: MonadHandler m => Feed Text -> m RepRss
|
||||||
|
rssFeedText feed = return $ RepRss $ toContent $ renderLBS def $ template feed id
|
||||||
|
|
||||||
template :: Feed url -> (url -> Text) -> Document
|
template :: Feed url -> (url -> Text) -> Document
|
||||||
template Feed {..} render =
|
template Feed {..} render =
|
||||||
Document (Prologue [] Nothing []) root []
|
Document (Prologue [] Nothing []) root []
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-newsfeed
|
name: yesod-newsfeed
|
||||||
version: 1.2.0
|
version: 1.2.0.2
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman, Patrick Brisbin
|
author: Michael Snoyman, Patrick Brisbin
|
||||||
@ -16,7 +16,8 @@ library
|
|||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, yesod-core >= 1.2 && < 1.3
|
, yesod-core >= 1.2 && < 1.3
|
||||||
, time >= 1.1.4
|
, time >= 1.1.4
|
||||||
, hamlet >= 1.1 && < 1.2
|
, hamlet >= 1.1
|
||||||
|
, shakespeare
|
||||||
, bytestring >= 0.9.1.4
|
, bytestring >= 0.9.1.4
|
||||||
, text >= 0.9
|
, text >= 0.9
|
||||||
, xml-conduit >= 1.0
|
, xml-conduit >= 1.0
|
||||||
|
|||||||
@ -25,16 +25,17 @@ import Control.Monad.Trans.Reader (runReaderT)
|
|||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Data.Conduit
|
import Data.Conduit
|
||||||
import Blaze.ByteString.Builder (Builder)
|
import Blaze.ByteString.Builder (Builder)
|
||||||
import Data.IORef.Lifted
|
import Data.Pool
|
||||||
import Data.Conduit.Pool
|
|
||||||
import Control.Monad.Trans.Resource
|
import Control.Monad.Trans.Resource
|
||||||
|
import Control.Exception (throwIO)
|
||||||
|
import Yesod.Core.Types (HandlerContents (HCError))
|
||||||
import qualified Database.Persist.Sql as SQL
|
import qualified Database.Persist.Sql as SQL
|
||||||
|
|
||||||
type YesodDB site = YesodPersistBackend site (HandlerT site IO)
|
type YesodDB site = YesodPersistBackend site (HandlerT site IO)
|
||||||
|
|
||||||
class Monad (YesodPersistBackend site (HandlerT site IO)) => YesodPersist site where
|
class Monad (YesodPersistBackend site (HandlerT site IO)) => YesodPersist site where
|
||||||
type YesodPersistBackend site :: (* -> *) -> * -> *
|
type YesodPersistBackend site :: (* -> *) -> * -> *
|
||||||
runDB :: YesodDB site a -> HandlerT site IO a
|
runDB :: YesodPersistBackend site (HandlerT site IO) a -> HandlerT site IO a
|
||||||
|
|
||||||
-- | Helper for creating 'runDB'.
|
-- | Helper for creating 'runDB'.
|
||||||
--
|
--
|
||||||
@ -70,7 +71,7 @@ class YesodPersist site => YesodPersistRunner site where
|
|||||||
getDBRunner :: HandlerT site IO (DBRunner site, HandlerT site IO ())
|
getDBRunner :: HandlerT site IO (DBRunner site, HandlerT site IO ())
|
||||||
|
|
||||||
newtype DBRunner site = DBRunner
|
newtype DBRunner site = DBRunner
|
||||||
{ runDBRunner :: forall a. YesodDB site a -> HandlerT site IO a
|
{ runDBRunner :: forall a. YesodPersistBackend site (HandlerT site IO) a -> HandlerT site IO a
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Helper for implementing 'getDBRunner'.
|
-- | Helper for implementing 'getDBRunner'.
|
||||||
@ -80,24 +81,23 @@ defaultGetDBRunner :: YesodPersistBackend site ~ SqlPersistT
|
|||||||
=> (site -> Pool SQL.Connection)
|
=> (site -> Pool SQL.Connection)
|
||||||
-> HandlerT site IO (DBRunner site, HandlerT site IO ())
|
-> HandlerT site IO (DBRunner site, HandlerT site IO ())
|
||||||
defaultGetDBRunner getPool = do
|
defaultGetDBRunner getPool = do
|
||||||
ididSucceed <- newIORef False
|
|
||||||
|
|
||||||
pool <- fmap getPool getYesod
|
pool <- fmap getPool getYesod
|
||||||
managedConn <- takeResource pool
|
let withPrep conn f = f conn (SQL.connPrepare conn)
|
||||||
let conn = mrValue managedConn
|
(relKey, (conn, local)) <- allocate
|
||||||
|
(do
|
||||||
|
(conn, local) <- takeResource pool
|
||||||
|
withPrep conn SQL.connBegin
|
||||||
|
return (conn, local)
|
||||||
|
)
|
||||||
|
(\(conn, local) -> do
|
||||||
|
withPrep conn SQL.connRollback
|
||||||
|
destroyResource pool local conn)
|
||||||
|
|
||||||
let withPrep f = f conn (SQL.connPrepare conn)
|
let cleanup = liftIO $ do
|
||||||
(finishTransaction, ()) <- allocate (withPrep SQL.connBegin) $ \() -> do
|
withPrep conn SQL.connCommit
|
||||||
didSucceed <- readIORef ididSucceed
|
putResource local conn
|
||||||
withPrep $ if didSucceed
|
_ <- unprotect relKey
|
||||||
then SQL.connCommit
|
return ()
|
||||||
else SQL.connRollback
|
|
||||||
|
|
||||||
let cleanup = do
|
|
||||||
writeIORef ididSucceed True
|
|
||||||
release finishTransaction
|
|
||||||
mrReuse managedConn True
|
|
||||||
mrRelease managedConn
|
|
||||||
|
|
||||||
return (DBRunner $ \x -> runReaderT (unSqlPersistT x) conn, cleanup)
|
return (DBRunner $ \x -> runReaderT (unSqlPersistT x) conn, cleanup)
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ defaultGetDBRunner getPool = do
|
|||||||
--
|
--
|
||||||
-- Since 1.2.0
|
-- Since 1.2.0
|
||||||
runDBSource :: YesodPersistRunner site
|
runDBSource :: YesodPersistRunner site
|
||||||
=> Source (YesodDB site) a
|
=> Source (YesodPersistBackend site (HandlerT site IO)) a
|
||||||
-> Source (HandlerT site IO) a
|
-> Source (HandlerT site IO) a
|
||||||
runDBSource src = do
|
runDBSource src = do
|
||||||
(dbrunner, cleanup) <- lift getDBRunner
|
(dbrunner, cleanup) <- lift getDBRunner
|
||||||
@ -116,7 +116,7 @@ runDBSource src = do
|
|||||||
-- | Extends 'respondSource' to create a streaming database response body.
|
-- | Extends 'respondSource' to create a streaming database response body.
|
||||||
respondSourceDB :: YesodPersistRunner site
|
respondSourceDB :: YesodPersistRunner site
|
||||||
=> ContentType
|
=> ContentType
|
||||||
-> Source (YesodDB site) (Flush Builder)
|
-> Source (YesodPersistBackend site (HandlerT site IO)) (Flush Builder)
|
||||||
-> HandlerT site IO TypedContent
|
-> HandlerT site IO TypedContent
|
||||||
respondSourceDB ctype = respondSource ctype . runDBSource
|
respondSourceDB ctype = respondSource ctype . runDBSource
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ get404 :: ( PersistStore (t m)
|
|||||||
get404 key = do
|
get404 key = do
|
||||||
mres <- get key
|
mres <- get key
|
||||||
case mres of
|
case mres of
|
||||||
Nothing -> lift notFound
|
Nothing -> notFound'
|
||||||
Just res -> return res
|
Just res -> return res
|
||||||
|
|
||||||
-- | Get the given entity by unique key, or return a 404 not found if it doesn't
|
-- | Get the given entity by unique key, or return a 404 not found if it doesn't
|
||||||
@ -148,9 +148,14 @@ getBy404 :: ( PersistUnique (t m)
|
|||||||
getBy404 key = do
|
getBy404 key = do
|
||||||
mres <- getBy key
|
mres <- getBy key
|
||||||
case mres of
|
case mres of
|
||||||
Nothing -> lift notFound
|
Nothing -> notFound'
|
||||||
Just res -> return res
|
Just res -> return res
|
||||||
|
|
||||||
|
-- | Should be equivalent to @lift . notFound@, but there's an apparent bug in
|
||||||
|
-- GHC 7.4.2 that leads to segfaults. This is a workaround.
|
||||||
|
notFound' :: MonadIO m => m a
|
||||||
|
notFound' = liftIO $ throwIO $ HCError NotFound
|
||||||
|
|
||||||
instance MonadHandler m => MonadHandler (SqlPersistT m) where
|
instance MonadHandler m => MonadHandler (SqlPersistT m) where
|
||||||
type HandlerSite (SqlPersistT m) = HandlerSite m
|
type HandlerSite (SqlPersistT m) = HandlerSite m
|
||||||
liftHandlerT = lift . liftHandlerT
|
liftHandlerT = lift . liftHandlerT
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-persistent
|
name: yesod-persistent
|
||||||
version: 1.2.1
|
version: 1.2.3
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -15,14 +15,13 @@ description: Some helpers for using Persistent from Yesod.
|
|||||||
library
|
library
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, yesod-core >= 1.2.2 && < 1.3
|
, yesod-core >= 1.2.2 && < 1.3
|
||||||
, persistent >= 1.2 && < 1.3
|
, persistent >= 1.2 && < 1.4
|
||||||
, persistent-template >= 1.2 && < 1.3
|
, persistent-template >= 1.2 && < 1.4
|
||||||
, transformers >= 0.2.2 && < 0.4
|
, transformers >= 0.2.2
|
||||||
, blaze-builder
|
, blaze-builder
|
||||||
, conduit
|
, conduit
|
||||||
, lifted-base
|
, resourcet >= 0.4.5
|
||||||
, pool-conduit
|
, resource-pool
|
||||||
, resourcet
|
|
||||||
exposed-modules: Yesod.Persist
|
exposed-modules: Yesod.Persist
|
||||||
Yesod.Persist.Core
|
Yesod.Persist.Core
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
@ -35,6 +34,7 @@ test-suite test
|
|||||||
build-depends: base
|
build-depends: base
|
||||||
, hspec
|
, hspec
|
||||||
, wai-test
|
, wai-test
|
||||||
|
, wai-extra
|
||||||
, yesod-core
|
, yesod-core
|
||||||
, persistent-sqlite
|
, persistent-sqlite
|
||||||
, yesod-persistent
|
, yesod-persistent
|
||||||
|
|||||||
@ -7,4 +7,16 @@ then
|
|||||||
cabal install cabal-nirvana -fgenerate
|
cabal install cabal-nirvana -fgenerate
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cabal-nirvana-generate yesod yesod-static hjsmin blaze-html yesod-test shakespeare-text | runghc to-cabal.hs > yesod-platform.cabal
|
cabal-nirvana-generate \
|
||||||
|
yesod \
|
||||||
|
yesod-static \
|
||||||
|
yesod-auth-hashdb \
|
||||||
|
hjsmin \
|
||||||
|
blaze-html \
|
||||||
|
yesod-test \
|
||||||
|
shakespeare-text \
|
||||||
|
esqueleto \
|
||||||
|
warp-tls \
|
||||||
|
hjsmin \
|
||||||
|
http-reverse-proxy \
|
||||||
|
| runghc to-cabal.hs > yesod-platform.cabal
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Control.Applicative ((<$>))
|
|||||||
|
|
||||||
main = do
|
main = do
|
||||||
pkgs <- map (intercalate " == ")
|
pkgs <- map (intercalate " == ")
|
||||||
. filter (\xs -> not $ any (`isPrefixOf` xs) $ map return ["parsec", "text", "transformers", "mtl", "HUnit", "QuickCheck", "binary", "zlib", "stm", "regex-compat", "hashable"])
|
. filter (\xs -> not $ any (`isPrefixOf` xs) $ map return ["parsec", "text", "transformers", "mtl", "HUnit", "QuickCheck", "binary", "zlib", "stm", "regex-compat", "hashable", "vault", "integer-gmp"])
|
||||||
. map words
|
. map words
|
||||||
. filter (not . null)
|
. filter (not . null)
|
||||||
. lines
|
. lines
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-platform
|
name: yesod-platform
|
||||||
version: 1.2.3
|
version: 1.2.12.2
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -14,131 +14,146 @@ homepage: http://www.yesodweb.com/
|
|||||||
|
|
||||||
library
|
library
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, SHA == 1.6.1
|
, SHA == 1.6.4
|
||||||
, aeson == 0.6.1.0
|
, aeson == 0.7.0.6
|
||||||
, ansi-terminal == 0.6
|
, ansi-terminal == 0.6.1.1
|
||||||
, asn1-data == 0.7.1
|
, ansi-wl-pprint == 0.6.7.1
|
||||||
, asn1-types == 0.2.0
|
, asn1-encoding == 0.8.1.3
|
||||||
, attoparsec == 0.10.4.0
|
, asn1-parse == 0.8.1
|
||||||
, attoparsec-conduit == 1.0.1.2
|
, asn1-types == 0.2.3
|
||||||
, authenticate == 1.3.2.6
|
, async == 2.0.1.5
|
||||||
, base-unicode-symbols == 0.2.2.4
|
, attoparsec == 0.12.0.0
|
||||||
|
, attoparsec-conduit == 1.1.0
|
||||||
|
, authenticate == 1.3.2.8
|
||||||
|
, base16-bytestring == 0.1.1.6
|
||||||
, base64-bytestring == 1.0.0.1
|
, base64-bytestring == 1.0.0.1
|
||||||
, blaze-builder == 0.3.1.1
|
, blaze-builder == 0.3.3.2
|
||||||
, blaze-builder-conduit == 1.0.0
|
, blaze-builder-conduit == 1.1.0
|
||||||
, blaze-html == 0.6.1.1
|
, blaze-html == 0.7.0.2
|
||||||
, blaze-markup == 0.5.1.5
|
, blaze-markup == 0.6.1.0
|
||||||
, byteable == 0.1.1
|
, byteable == 0.1.1
|
||||||
, byteorder == 1.0.4
|
, byteorder == 1.0.4
|
||||||
, case-insensitive == 1.0.0.2
|
, case-insensitive == 1.2.0.0
|
||||||
, cereal == 0.3.5.2
|
, cereal == 0.4.0.1
|
||||||
, certificate == 1.3.8
|
, cipher-aes == 0.2.7
|
||||||
, cipher-aes == 0.1.8
|
, cipher-des == 0.0.6
|
||||||
, cipher-rc4 == 0.1.2
|
, cipher-rc4 == 0.1.4
|
||||||
, clientsession == 0.9
|
, clientsession == 0.9.0.3
|
||||||
, conduit == 1.0.7.3
|
, conduit == 1.1.6
|
||||||
, cookie == 0.4.0.1
|
, conduit-extra == 1.1.0.4
|
||||||
, cprng-aes == 0.3.4
|
, connection == 0.2.1
|
||||||
, crypto-api == 0.12.2.1
|
, cookie == 0.4.1.1
|
||||||
, crypto-conduit == 0.5.2
|
, cprng-aes == 0.5.2
|
||||||
, crypto-numbers == 0.1.3
|
, crypto-api == 0.13
|
||||||
, crypto-pubkey == 0.1.4
|
, crypto-cipher-types == 0.0.9
|
||||||
, crypto-pubkey-types == 0.4.0
|
, crypto-numbers == 0.2.3
|
||||||
, crypto-random-api == 0.2.0
|
, crypto-pubkey == 0.2.4
|
||||||
, cryptohash == 0.9.1
|
, crypto-pubkey-types == 0.4.2.2
|
||||||
, css-text == 0.1.1
|
, crypto-random == 0.0.7
|
||||||
|
, cryptohash == 0.11.5
|
||||||
|
, cryptohash-conduit == 0.1.1
|
||||||
|
, css-text == 0.1.2.1
|
||||||
, data-default == 0.5.3
|
, data-default == 0.5.3
|
||||||
, data-default-class == 0.0.1
|
, data-default-class == 0.0.1
|
||||||
, data-default-instances-base == 0.0.1
|
, data-default-instances-base == 0.0.1
|
||||||
, data-default-instances-containers == 0.0.1
|
, data-default-instances-containers == 0.0.1
|
||||||
, data-default-instances-dlist == 0.0.1
|
, data-default-instances-dlist == 0.0.1
|
||||||
, data-default-instances-old-locale == 0.0.1
|
, data-default-instances-old-locale == 0.0.1
|
||||||
, date-cache == 0.3.0
|
, dlist == 0.7.0.1
|
||||||
, dlist == 0.5
|
, email-validate == 2.0.1
|
||||||
, email-validate == 1.0.0
|
, entropy == 0.3.2
|
||||||
, entropy == 0.2.2.1
|
, esqueleto == 1.4.1.2
|
||||||
, failure == 0.2.0.1
|
, exceptions == 0.6.1
|
||||||
, fast-logger == 0.3.2
|
, fast-logger == 2.1.5
|
||||||
, file-embed == 0.0.4.9
|
, file-embed == 0.0.7
|
||||||
, filesystem-conduit == 1.0.0.1
|
, hamlet == 1.2.0
|
||||||
, hamlet == 1.1.7.1
|
, hjsmin == 0.1.4.6
|
||||||
, hjsmin == 0.1.4.1
|
, hspec == 1.9.5
|
||||||
, hspec == 1.6.1
|
, hspec-expectations == 0.5.0.1
|
||||||
, hspec-expectations == 0.3.2
|
, html-conduit == 1.1.0.5
|
||||||
, html-conduit == 1.1.0
|
, http-client == 0.3.3
|
||||||
, http-attoparsec == 0.1.0
|
, http-client-tls == 0.2.1.1
|
||||||
, http-conduit == 1.9.4.1
|
, http-conduit == 2.1.2
|
||||||
, http-date == 0.0.4
|
, http-date == 0.0.4
|
||||||
, http-types == 0.8.0
|
, http-reverse-proxy == 0.3.1.8
|
||||||
, language-javascript == 0.5.7
|
, http-types == 0.8.5
|
||||||
, lifted-base == 0.2.1.0
|
, language-javascript == 0.5.13
|
||||||
, mime-mail == 0.4.2
|
, lifted-base == 0.2.2.2
|
||||||
, mime-types == 0.1.0.3
|
, mime-mail == 0.4.5.2
|
||||||
, mmorph == 1.0.0
|
, mime-types == 0.1.0.4
|
||||||
, monad-control == 0.3.2.1
|
, mmorph == 1.0.3
|
||||||
, monad-logger == 0.3.1.1
|
, monad-control == 0.3.3.0
|
||||||
, network-conduit == 1.0.0
|
, monad-logger == 0.3.6.1
|
||||||
, path-pieces == 0.1.2
|
, monad-loops == 0.4.2
|
||||||
, pem == 0.1.2
|
, nats == 0.2
|
||||||
, persistent == 1.2.2.0
|
, network-conduit == 1.1.0
|
||||||
, persistent-template == 1.2.0.2
|
, optparse-applicative == 0.8.1
|
||||||
, pool-conduit == 0.1.2
|
, path-pieces == 0.1.3.1
|
||||||
, primitive == 0.5.0.1
|
, pem == 0.2.2
|
||||||
|
, persistent == 1.3.1.1
|
||||||
|
, persistent-template == 1.3.1.4
|
||||||
|
, primitive == 0.5.3.0
|
||||||
, publicsuffixlist == 0.1
|
, publicsuffixlist == 0.1
|
||||||
, pureMD5 == 2.1.2.1
|
, pwstore-fast == 2.4.1
|
||||||
, pwstore-fast == 2.3
|
, quickcheck-io == 0.1.1
|
||||||
, quickcheck-io == 0.1.0
|
, resource-pool == 0.2.3.0
|
||||||
, resource-pool == 0.2.1.1
|
, resourcet == 1.1.2.2
|
||||||
, resourcet == 0.4.7.1
|
, safe == 0.3.4
|
||||||
, safe == 0.3.3
|
, scientific == 0.3.2.1
|
||||||
, semigroups == 0.9.2
|
, securemem == 0.1.3
|
||||||
, setenv == 0.1.0
|
, semigroups == 0.15
|
||||||
, shakespeare == 1.0.5
|
, setenv == 0.1.1.1
|
||||||
, shakespeare-css == 1.0.6.2
|
, shakespeare == 2.0.0.3
|
||||||
, shakespeare-i18n == 1.0.0.3
|
, shakespeare-css == 1.1.0
|
||||||
, shakespeare-js == 1.1.4.1
|
, shakespeare-i18n == 1.1.0
|
||||||
, shakespeare-text == 1.0.0.6
|
, shakespeare-js == 1.3.0
|
||||||
|
, shakespeare-text == 1.1.0
|
||||||
, silently == 1.2.4.1
|
, silently == 1.2.4.1
|
||||||
, simple-sendfile == 0.2.12
|
, simple-sendfile == 0.2.14
|
||||||
, skein == 1.0.3
|
, skein == 1.0.9
|
||||||
, socks == 0.5.1
|
, socks == 0.5.4
|
||||||
, stringsearch == 0.3.6.4
|
, stm-chans == 3.0.0.2
|
||||||
, system-fileio == 0.3.11
|
, streaming-commons == 0.1.3
|
||||||
, system-filepath == 0.4.7
|
, stringsearch == 0.3.6.5
|
||||||
, tagged == 0.6.1
|
, system-fileio == 0.3.14
|
||||||
, tagsoup == 0.12.8
|
, system-filepath == 0.4.12
|
||||||
, tagstream-conduit == 0.5.4
|
, tagged == 0.7.2
|
||||||
, tls == 1.1.2
|
, tagsoup == 0.13.1
|
||||||
, tls-extra == 0.6.4
|
, tagstream-conduit == 0.5.5.1
|
||||||
, transformers-base == 0.4.1
|
, tf-random == 0.5
|
||||||
|
, tls == 1.2.8
|
||||||
|
, transformers-base == 0.4.2
|
||||||
|
-- , transformers-compat == 0.3.3.4
|
||||||
, unix-compat == 0.4.1.1
|
, unix-compat == 0.4.1.1
|
||||||
, unordered-containers == 0.2.3.1
|
, unordered-containers == 0.2.4.0
|
||||||
, utf8-light == 0.4.0.1
|
, utf8-string == 0.3.8
|
||||||
, utf8-string == 0.3.7
|
, vector == 0.10.11.0
|
||||||
, vault == 0.2.0.4
|
|
||||||
, vector == 0.10.0.1
|
|
||||||
, void == 0.6.1
|
, void == 0.6.1
|
||||||
, wai == 1.4.0.1
|
, wai == 3.0.0
|
||||||
, wai-app-static == 1.3.1.3
|
, wai-app-static == 3.0.0
|
||||||
, wai-extra == 1.3.4.2
|
, wai-extra == 3.0.0
|
||||||
, wai-logger == 0.3.1
|
, wai-logger == 2.1.1
|
||||||
, wai-test == 1.3.1.1
|
, wai-test == 3.0.0
|
||||||
, warp == 1.3.9
|
, warp == 3.0.0.2
|
||||||
, word8 == 0.0.3
|
, warp-tls == 3.0.0
|
||||||
, xml-conduit == 1.1.0.5
|
, word8 == 0.0.4
|
||||||
|
, x509 == 1.4.11
|
||||||
|
, x509-store == 1.4.4
|
||||||
|
, x509-system == 1.4.5
|
||||||
|
, x509-validation == 1.5.0
|
||||||
|
, xml-conduit == 1.2.0.2
|
||||||
, xml-types == 0.3.4
|
, xml-types == 0.3.4
|
||||||
, xss-sanitize == 0.3.4
|
, xss-sanitize == 0.3.5.2
|
||||||
, yaml == 0.8.4
|
, yaml == 0.8.8.3
|
||||||
, yesod == 1.2.1.1
|
, yesod == 1.2.6
|
||||||
, yesod-auth == 1.2.0.2
|
, yesod-auth == 1.3.1
|
||||||
, yesod-core == 1.2.3
|
, yesod-auth-hashdb == 1.3.0.1
|
||||||
, yesod-form == 1.3.0.1
|
, yesod-core == 1.2.16
|
||||||
, yesod-persistent == 1.2.1
|
, yesod-form == 1.3.10
|
||||||
, yesod-routes == 1.2.0.1
|
, yesod-persistent == 1.2.3
|
||||||
, yesod-static == 1.2.0
|
, yesod-routes == 1.2.0.6
|
||||||
, yesod-test == 1.2.0
|
, yesod-static == 1.2.4
|
||||||
, zlib-bindings == 0.1.1.3
|
, yesod-test == 1.2.3
|
||||||
, zlib-conduit == 1.0.0
|
|
||||||
|
|
||||||
exposed-modules: Yesod.Platform
|
exposed-modules: Yesod.Platform
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,42 @@ module Yesod.Routes.Overlap
|
|||||||
import Yesod.Routes.TH.Types
|
import Yesod.Routes.TH.Types
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
|
|
||||||
|
data Flattened t = Flattened
|
||||||
|
{ fNames :: [String]
|
||||||
|
, fPieces :: [(CheckOverlap, Piece t)]
|
||||||
|
, fHasSuffix :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten :: ResourceTree t -> [Flattened t]
|
||||||
|
flatten =
|
||||||
|
go id id
|
||||||
|
where
|
||||||
|
go names pieces (ResourceLeaf r) = return Flattened
|
||||||
|
{ fNames = names [resourceName r]
|
||||||
|
, fPieces = pieces (resourcePieces r)
|
||||||
|
, fHasSuffix = hasSuffix $ ResourceLeaf r
|
||||||
|
}
|
||||||
|
go names pieces (ResourceParent newname newpieces children) =
|
||||||
|
concatMap (go names' pieces') children
|
||||||
|
where
|
||||||
|
names' = names . (newname:)
|
||||||
|
pieces' = pieces . (newpieces ++)
|
||||||
|
|
||||||
data Overlap t = Overlap
|
data Overlap t = Overlap
|
||||||
{ overlapParents :: [String] -> [String] -- ^ parent resource trees
|
{ overlapParents :: [String] -> [String] -- ^ parent resource trees
|
||||||
, overlap1 :: ResourceTree t
|
, overlap1 :: ResourceTree t
|
||||||
, overlap2 :: ResourceTree t
|
, overlap2 :: ResourceTree t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data OverlapF = OverlapF
|
||||||
|
{ overlapF1 :: [String]
|
||||||
|
, overlapF2 :: [String]
|
||||||
|
}
|
||||||
|
|
||||||
findOverlaps :: ([String] -> [String]) -> [ResourceTree t] -> [Overlap t]
|
findOverlaps :: ([String] -> [String]) -> [ResourceTree t] -> [Overlap t]
|
||||||
findOverlaps _ [] = []
|
findOverlaps _ [] = []
|
||||||
findOverlaps front (x:xs) = concatMap (findOverlap front x) xs ++ findOverlaps front xs
|
findOverlaps front (x:xs) = concatMap (findOverlap front x) xs ++ findOverlaps front xs
|
||||||
|
{-# DEPRECATED findOverlaps "This function is no longer used" #-}
|
||||||
|
|
||||||
findOverlap :: ([String] -> [String]) -> ResourceTree t -> ResourceTree t -> [Overlap t]
|
findOverlap :: ([String] -> [String]) -> ResourceTree t -> ResourceTree t -> [Overlap t]
|
||||||
findOverlap front x y =
|
findOverlap front x y =
|
||||||
@ -30,14 +57,6 @@ findOverlap front x y =
|
|||||||
ResourceParent name _ children -> findOverlaps (front . (name:)) children
|
ResourceParent name _ children -> findOverlaps (front . (name:)) children
|
||||||
ResourceLeaf{} -> []
|
ResourceLeaf{} -> []
|
||||||
|
|
||||||
hasSuffix :: ResourceTree t -> Bool
|
|
||||||
hasSuffix (ResourceLeaf r) =
|
|
||||||
case resourceDispatch r of
|
|
||||||
Subsite{} -> True
|
|
||||||
Methods Just{} _ -> True
|
|
||||||
Methods Nothing _ -> False
|
|
||||||
hasSuffix ResourceParent{} = True
|
|
||||||
|
|
||||||
overlaps :: [(CheckOverlap, Piece t)] -> [(CheckOverlap, Piece t)] -> Bool -> Bool -> Bool
|
overlaps :: [(CheckOverlap, Piece t)] -> [(CheckOverlap, Piece t)] -> Bool -> Bool -> Bool
|
||||||
|
|
||||||
-- No pieces on either side, will overlap regardless of suffix
|
-- No pieces on either side, will overlap regardless of suffix
|
||||||
@ -66,9 +85,26 @@ piecesOverlap _ _ = True
|
|||||||
|
|
||||||
findOverlapNames :: [ResourceTree t] -> [(String, String)]
|
findOverlapNames :: [ResourceTree t] -> [(String, String)]
|
||||||
findOverlapNames =
|
findOverlapNames =
|
||||||
map go . findOverlaps id
|
map go . findOverlapsF . concatMap Yesod.Routes.Overlap.flatten
|
||||||
where
|
where
|
||||||
go (Overlap front x y) =
|
go (OverlapF x y) =
|
||||||
(go' $ resourceTreeName x, go' $ resourceTreeName y)
|
(go' x, go' y)
|
||||||
where
|
where
|
||||||
go' = intercalate "/" . front . return
|
go' = intercalate "/"
|
||||||
|
|
||||||
|
findOverlapsF :: [Flattened t] -> [OverlapF]
|
||||||
|
findOverlapsF [] = []
|
||||||
|
findOverlapsF (x:xs) = concatMap (findOverlapF x) xs ++ findOverlapsF xs
|
||||||
|
|
||||||
|
findOverlapF :: Flattened t -> Flattened t -> [OverlapF]
|
||||||
|
findOverlapF x y
|
||||||
|
| overlaps (fPieces x) (fPieces y) (fHasSuffix x) (fHasSuffix y) = [OverlapF (fNames x) (fNames y)]
|
||||||
|
| otherwise = []
|
||||||
|
|
||||||
|
hasSuffix :: ResourceTree t -> Bool
|
||||||
|
hasSuffix (ResourceLeaf r) =
|
||||||
|
case resourceDispatch r of
|
||||||
|
Subsite{} -> True
|
||||||
|
Methods Just{} _ -> True
|
||||||
|
Methods Nothing _ -> False
|
||||||
|
hasSuffix ResourceParent{} = True
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE PatternGuards #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter
|
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter
|
||||||
module Yesod.Routes.Parse
|
module Yesod.Routes.Parse
|
||||||
( parseRoutes
|
( parseRoutes
|
||||||
@ -18,6 +19,8 @@ import qualified System.IO as SIO
|
|||||||
import Yesod.Routes.TH
|
import Yesod.Routes.TH
|
||||||
import Yesod.Routes.Overlap (findOverlapNames)
|
import Yesod.Routes.Overlap (findOverlapNames)
|
||||||
import Data.List (foldl')
|
import Data.List (foldl')
|
||||||
|
import Data.Maybe (mapMaybe)
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
-- | A quasi-quoter to parse a string into a list of 'Resource's. Checks for
|
-- | A quasi-quoter to parse a string into a list of 'Resource's. Checks for
|
||||||
-- overlapping routes, failing if present; use 'parseRoutesNoCheck' to skip the
|
-- overlapping routes, failing if present; use 'parseRoutesNoCheck' to skip the
|
||||||
@ -29,7 +32,7 @@ parseRoutes = QuasiQuoter { quoteExp = x }
|
|||||||
let res = resourcesFromString s
|
let res = resourcesFromString s
|
||||||
case findOverlapNames res of
|
case findOverlapNames res of
|
||||||
[] -> lift res
|
[] -> lift res
|
||||||
z -> error $ "Overlapping routes: " ++ unlines (map show z)
|
z -> error $ unlines $ "Overlapping routes: " : map show z
|
||||||
|
|
||||||
parseRoutesFile :: FilePath -> Q Exp
|
parseRoutesFile :: FilePath -> Q Exp
|
||||||
parseRoutesFile = parseRoutesFileWith parseRoutes
|
parseRoutesFile = parseRoutesFileWith parseRoutes
|
||||||
@ -60,21 +63,37 @@ parseRoutesNoCheck = QuasiQuoter
|
|||||||
-- invalid input.
|
-- invalid input.
|
||||||
resourcesFromString :: String -> [ResourceTree String]
|
resourcesFromString :: String -> [ResourceTree String]
|
||||||
resourcesFromString =
|
resourcesFromString =
|
||||||
fst . parse 0 . lines
|
fst . parse 0 . filter (not . all (== ' ')) . lines
|
||||||
where
|
where
|
||||||
parse _ [] = ([], [])
|
parse _ [] = ([], [])
|
||||||
parse indent (thisLine:otherLines)
|
parse indent (thisLine:otherLines)
|
||||||
| length spaces < indent = ([], thisLine : otherLines)
|
| length spaces < indent = ([], thisLine : otherLines)
|
||||||
| otherwise = (this others, remainder)
|
| otherwise = (this others, remainder)
|
||||||
where
|
where
|
||||||
|
parseAttr ('!':x) = Just x
|
||||||
|
parseAttr _ = Nothing
|
||||||
|
|
||||||
|
stripColonLast =
|
||||||
|
go id
|
||||||
|
where
|
||||||
|
go _ [] = Nothing
|
||||||
|
go front [x]
|
||||||
|
| null x = Nothing
|
||||||
|
| last x == ':' = Just $ front [init x]
|
||||||
|
| otherwise = Nothing
|
||||||
|
go front (x:xs) = go (front . (x:)) xs
|
||||||
|
|
||||||
spaces = takeWhile (== ' ') thisLine
|
spaces = takeWhile (== ' ') thisLine
|
||||||
(others, remainder) = parse indent otherLines'
|
(others, remainder) = parse indent otherLines'
|
||||||
(this, otherLines') =
|
(this, otherLines') =
|
||||||
case takeWhile (/= "--") $ words thisLine of
|
case takeWhile (/= "--") $ words thisLine of
|
||||||
[pattern, constr] | last constr == ':' ->
|
(pattern:rest0)
|
||||||
|
| Just (constr:rest) <- stripColonLast rest0
|
||||||
|
, Just attrs <- mapM parseAttr rest ->
|
||||||
let (children, otherLines'') = parse (length spaces + 1) otherLines
|
let (children, otherLines'') = parse (length spaces + 1) otherLines
|
||||||
|
children' = addAttrs attrs children
|
||||||
(pieces, Nothing) = piecesFromString $ drop1Slash pattern
|
(pieces, Nothing) = piecesFromString $ drop1Slash pattern
|
||||||
in ((ResourceParent (init constr) pieces children :), otherLines'')
|
in ((ResourceParent constr pieces children' :), otherLines'')
|
||||||
(pattern:constr:rest) ->
|
(pattern:constr:rest) ->
|
||||||
let (pieces, mmulti) = piecesFromString $ drop1Slash pattern
|
let (pieces, mmulti) = piecesFromString $ drop1Slash pattern
|
||||||
(attrs, rest') = takeAttrs rest
|
(attrs, rest') = takeAttrs rest
|
||||||
@ -83,6 +102,29 @@ resourcesFromString =
|
|||||||
[] -> (id, otherLines)
|
[] -> (id, otherLines)
|
||||||
_ -> error $ "Invalid resource line: " ++ thisLine
|
_ -> error $ "Invalid resource line: " ++ thisLine
|
||||||
|
|
||||||
|
addAttrs :: [String] -> [ResourceTree String] -> [ResourceTree String]
|
||||||
|
addAttrs attrs =
|
||||||
|
map goTree
|
||||||
|
where
|
||||||
|
goTree (ResourceLeaf res) = ResourceLeaf (goRes res)
|
||||||
|
goTree (ResourceParent x y z) = ResourceParent x y (map goTree z)
|
||||||
|
|
||||||
|
goRes res =
|
||||||
|
res { resourceAttrs = noDupes ++ resourceAttrs res }
|
||||||
|
where
|
||||||
|
usedKeys = Set.fromList $ map fst $ mapMaybe toPair $ resourceAttrs res
|
||||||
|
used attr =
|
||||||
|
case toPair attr of
|
||||||
|
Nothing -> False
|
||||||
|
Just (key, _) -> key `Set.member` usedKeys
|
||||||
|
noDupes = filter (not . used) attrs
|
||||||
|
|
||||||
|
toPair s =
|
||||||
|
case break (== '=') s of
|
||||||
|
(x, '=':y) -> Just (x, y)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
-- | Take attributes out of the list and put them in the first slot in the
|
-- | Take attributes out of the list and put them in the first slot in the
|
||||||
-- result tuple.
|
-- result tuple.
|
||||||
takeAttrs :: [String] -> ([String], [String])
|
takeAttrs :: [String] -> ([String], [String])
|
||||||
@ -184,6 +226,7 @@ ttToType (TTList t) = ListT `AppT` ttToType t
|
|||||||
|
|
||||||
pieceFromString :: String -> Either String (CheckOverlap, Piece String)
|
pieceFromString :: String -> Either String (CheckOverlap, Piece String)
|
||||||
pieceFromString ('#':'!':x) = Right $ (False, Dynamic x)
|
pieceFromString ('#':'!':x) = Right $ (False, Dynamic x)
|
||||||
|
pieceFromString ('!':'#':x) = Right $ (False, Dynamic x) -- https://github.com/yesodweb/yesod/issues/652
|
||||||
pieceFromString ('#':x) = Right $ (True, Dynamic x)
|
pieceFromString ('#':x) = Right $ (True, Dynamic x)
|
||||||
pieceFromString ('*':x) = Left x
|
pieceFromString ('*':x) = Left x
|
||||||
pieceFromString ('+':x) = Left x
|
pieceFromString ('+':x) = Left x
|
||||||
|
|||||||
@ -10,7 +10,7 @@ module Hierarchy
|
|||||||
( hierarchy
|
( hierarchy
|
||||||
, Dispatcher (..)
|
, Dispatcher (..)
|
||||||
, runHandler
|
, runHandler
|
||||||
, Handler
|
, Handler2
|
||||||
, App
|
, App
|
||||||
, toText
|
, toText
|
||||||
, Env (..)
|
, Env (..)
|
||||||
@ -24,9 +24,10 @@ import Yesod.Routes.TH
|
|||||||
import Yesod.Routes.Class
|
import Yesod.Routes.Class
|
||||||
import Language.Haskell.TH.Syntax
|
import Language.Haskell.TH.Syntax
|
||||||
import qualified Yesod.Routes.Class as YRC
|
import qualified Yesod.Routes.Class as YRC
|
||||||
import Data.Text (Text, pack, append)
|
import Data.Text (Text, pack, unpack, append)
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
class ToText a where
|
class ToText a where
|
||||||
toText :: a -> Text
|
toText :: a -> Text
|
||||||
@ -34,7 +35,9 @@ class ToText a where
|
|||||||
instance ToText Text where toText = id
|
instance ToText Text where toText = id
|
||||||
instance ToText String where toText = pack
|
instance ToText String where toText = pack
|
||||||
|
|
||||||
type Handler sub master a = a
|
type Handler2 sub master a = a
|
||||||
|
type Handler site a = Handler2 site site a
|
||||||
|
|
||||||
type Request = ([Text], ByteString) -- path info, method
|
type Request = ([Text], ByteString) -- path info, method
|
||||||
type App sub master = Request -> (Text, Maybe (YRC.Route master))
|
type App sub master = Request -> (Text, Maybe (YRC.Route master))
|
||||||
data Env sub master = Env
|
data Env sub master = Env
|
||||||
@ -45,7 +48,7 @@ data Env sub master = Env
|
|||||||
|
|
||||||
subDispatch
|
subDispatch
|
||||||
:: (Env sub master -> App sub master)
|
:: (Env sub master -> App sub master)
|
||||||
-> (Handler sub master Text -> Env sub master -> Maybe (YRC.Route sub) -> App sub master)
|
-> (Handler2 sub master Text -> Env sub master -> Maybe (YRC.Route sub) -> App sub master)
|
||||||
-> (master -> sub)
|
-> (master -> sub)
|
||||||
-> (YRC.Route sub -> YRC.Route master)
|
-> (YRC.Route sub -> YRC.Route master)
|
||||||
-> Env master master
|
-> Env master master
|
||||||
@ -63,24 +66,49 @@ class Dispatcher sub master where
|
|||||||
|
|
||||||
runHandler
|
runHandler
|
||||||
:: ToText a
|
:: ToText a
|
||||||
=> Handler sub master a
|
=> Handler2 sub master a
|
||||||
-> Env sub master
|
-> Env sub master
|
||||||
-> Maybe (Route sub)
|
-> Maybe (Route sub)
|
||||||
-> App sub master
|
-> App sub master
|
||||||
runHandler h Env {..} route _ = (toText h, fmap envToMaster route)
|
runHandler h Env {..} route _ = (toText h, fmap envToMaster route)
|
||||||
|
|
||||||
|
|
||||||
data Hierarchy = Hierarchy
|
data Hierarchy = Hierarchy
|
||||||
|
|
||||||
do
|
do
|
||||||
let resources = [parseRoutes|
|
let resources = [parseRoutes|
|
||||||
/ HomeR GET
|
/ HomeR GET
|
||||||
|
|
||||||
|
/!#Int BackwardsR GET
|
||||||
|
|
||||||
/admin/#Int AdminR:
|
/admin/#Int AdminR:
|
||||||
/ AdminRootR GET
|
/ AdminRootR GET
|
||||||
/login LoginR GET POST
|
/login LoginR GET POST
|
||||||
/table/#Text TableR GET
|
/table/#Text TableR GET
|
||||||
|
|
||||||
|
/nest/ NestR !NestingAttr:
|
||||||
|
|
||||||
|
/spaces SpacedR GET !NonNested
|
||||||
|
|
||||||
|
/nest2 Nest2:
|
||||||
|
/ GetPostR GET POST
|
||||||
|
/get Get2 GET
|
||||||
|
/post Post2 POST
|
||||||
|
-- /#Int Delete2 DELETE
|
||||||
|
/nest3 Nest3:
|
||||||
|
/get Get3 GET
|
||||||
|
/post Post3 POST
|
||||||
|
-- /#Int Delete3 DELETE
|
||||||
|
|
||||||
|
/afterwards AfterR !parent !key=value1:
|
||||||
|
/ After GET !child !key=value2
|
||||||
|
|
||||||
|
-- /trailing-nest TrailingNestR:
|
||||||
|
-- /foo TrailingFooR GET
|
||||||
|
-- /#Int TrailingIntR GET
|
||||||
|]
|
|]
|
||||||
|
|
||||||
rrinst <- mkRenderRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
|
rrinst <- mkRenderRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
|
||||||
|
rainst <- mkRouteAttrsInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
|
||||||
prinst <- mkParseRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
|
prinst <- mkParseRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
|
||||||
dispatch <- mkDispatchClause MkDispatchSettings
|
dispatch <- mkDispatchClause MkDispatchSettings
|
||||||
{ mdsRunHandler = [|runHandler|]
|
{ mdsRunHandler = [|runHandler|]
|
||||||
@ -100,25 +128,50 @@ do
|
|||||||
`AppT` ConT ''Hierarchy)
|
`AppT` ConT ''Hierarchy)
|
||||||
[FunD (mkName "dispatcher") [dispatch]]
|
[FunD (mkName "dispatcher") [dispatch]]
|
||||||
: prinst
|
: prinst
|
||||||
|
: rainst
|
||||||
: rrinst
|
: rrinst
|
||||||
|
|
||||||
getHomeR :: Handler sub master String
|
getSpacedR :: Handler site String
|
||||||
|
getSpacedR = "root-leaf"
|
||||||
|
|
||||||
|
getGet2 :: Handler site String; getGet2 = "get"
|
||||||
|
postPost2 :: Handler site String; postPost2 = "post"
|
||||||
|
deleteDelete2 :: Int -> Handler site String; deleteDelete2 = const "delete"
|
||||||
|
getGet3 :: Handler site String; getGet3 = "get"
|
||||||
|
postPost3 :: Handler site String; postPost3 = "post"
|
||||||
|
deleteDelete3 :: Int -> Handler site String; deleteDelete3 = const "delete"
|
||||||
|
|
||||||
|
getAfter :: Handler site String; getAfter = "after"
|
||||||
|
|
||||||
|
getHomeR :: Handler site String
|
||||||
getHomeR = "home"
|
getHomeR = "home"
|
||||||
|
|
||||||
getAdminRootR :: Int -> Handler sub master Text
|
getBackwardsR :: Int -> Handler site Text
|
||||||
|
getBackwardsR _ = pack "backwards"
|
||||||
|
|
||||||
|
getAdminRootR :: Int -> Handler site Text
|
||||||
getAdminRootR i = pack $ "admin root: " ++ show i
|
getAdminRootR i = pack $ "admin root: " ++ show i
|
||||||
|
|
||||||
getLoginR :: Int -> Handler sub master Text
|
getLoginR :: Int -> Handler site Text
|
||||||
getLoginR i = pack $ "login: " ++ show i
|
getLoginR i = pack $ "login: " ++ show i
|
||||||
|
|
||||||
postLoginR :: Int -> Handler sub master Text
|
postLoginR :: Int -> Handler site Text
|
||||||
postLoginR i = pack $ "post login: " ++ show i
|
postLoginR i = pack $ "post login: " ++ show i
|
||||||
|
|
||||||
getTableR :: Int -> Text -> Handler sub master Text
|
getTableR :: Int -> Text -> Handler site Text
|
||||||
getTableR _ t = append "TableR " t
|
getTableR _ = append "TableR "
|
||||||
|
|
||||||
|
getGetPostR :: Handler site Text
|
||||||
|
getGetPostR = pack "get"
|
||||||
|
|
||||||
|
postGetPostR :: Handler site Text
|
||||||
|
postGetPostR = pack "post"
|
||||||
|
|
||||||
|
|
||||||
hierarchy :: Spec
|
hierarchy :: Spec
|
||||||
hierarchy = describe "hierarchy" $ do
|
hierarchy = describe "hierarchy" $ do
|
||||||
|
it "nested with spacing" $
|
||||||
|
renderRoute (NestR SpacedR) @?= (["nest", "spaces"], [])
|
||||||
it "renders root correctly" $
|
it "renders root correctly" $
|
||||||
renderRoute (AdminR 5 AdminRootR) @?= (["admin", "5"], [])
|
renderRoute (AdminR 5 AdminRootR) @?= (["admin", "5"], [])
|
||||||
it "renders table correctly" $
|
it "renders table correctly" $
|
||||||
@ -130,6 +183,18 @@ hierarchy = describe "hierarchy" $ do
|
|||||||
, envSub = Hierarchy
|
, envSub = Hierarchy
|
||||||
})
|
})
|
||||||
(map pack ps, S8.pack m)
|
(map pack ps, S8.pack m)
|
||||||
|
|
||||||
|
let testGetPost route getRes postRes = do
|
||||||
|
let routeStrs = map unpack $ fst (renderRoute route)
|
||||||
|
disp "GET" routeStrs @?= (getRes, Just route)
|
||||||
|
disp "POST" routeStrs @?= (postRes, Just route)
|
||||||
|
|
||||||
|
it "dispatches routes with multiple METHODs: admin" $
|
||||||
|
testGetPost (AdminR 1 LoginR) "login: 1" "post login: 1"
|
||||||
|
|
||||||
|
it "dispatches routes with multiple METHODs: nesting" $
|
||||||
|
testGetPost (NestR $ Nest2 GetPostR) "get" "post"
|
||||||
|
|
||||||
it "dispatches root correctly" $ disp "GET" ["admin", "7"] @?= ("admin root: 7", Just $ AdminR 7 AdminRootR)
|
it "dispatches root correctly" $ disp "GET" ["admin", "7"] @?= ("admin root: 7", Just $ AdminR 7 AdminRootR)
|
||||||
it "dispatches table correctly" $ disp "GET" ["admin", "8", "table", "bar"] @?= ("TableR bar", Just $ AdminR 8 $ TableR "bar")
|
it "dispatches table correctly" $ disp "GET" ["admin", "8", "table", "bar"] @?= ("TableR bar", Just $ AdminR 8 $ TableR "bar")
|
||||||
it "parses" $ do
|
it "parses" $ do
|
||||||
@ -137,3 +202,7 @@ hierarchy = describe "hierarchy" $ do
|
|||||||
parseRoute ([], [("foo", "bar")]) @?= Just HomeR
|
parseRoute ([], [("foo", "bar")]) @?= Just HomeR
|
||||||
parseRoute (["admin", "5"], []) @?= Just (AdminR 5 AdminRootR)
|
parseRoute (["admin", "5"], []) @?= Just (AdminR 5 AdminRootR)
|
||||||
parseRoute (["admin!", "5"], []) @?= (Nothing :: Maybe (Route Hierarchy))
|
parseRoute (["admin!", "5"], []) @?= (Nothing :: Maybe (Route Hierarchy))
|
||||||
|
it "inherited attributes" $ do
|
||||||
|
routeAttrs (NestR SpacedR) @?= Set.fromList ["NestingAttr", "NonNested"]
|
||||||
|
it "pair attributes" $
|
||||||
|
routeAttrs (AfterR After) @?= Set.fromList ["parent", "child", "key=value2"]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-routes
|
name: yesod-routes
|
||||||
version: 1.2.0.1
|
version: 1.2.0.7
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -16,7 +16,7 @@ extra-source-files:
|
|||||||
|
|
||||||
library
|
library
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, text >= 0.5 && < 0.12
|
, text >= 0.5
|
||||||
, vector >= 0.8 && < 0.11
|
, vector >= 0.8 && < 0.11
|
||||||
, containers >= 0.2
|
, containers >= 0.2
|
||||||
, template-haskell
|
, template-haskell
|
||||||
@ -42,7 +42,7 @@ test-suite runtests
|
|||||||
|
|
||||||
build-depends: base >= 4.3 && < 5
|
build-depends: base >= 4.3 && < 5
|
||||||
, yesod-routes
|
, yesod-routes
|
||||||
, text >= 0.5 && < 0.12
|
, text >= 0.5
|
||||||
, HUnit >= 1.2 && < 1.3
|
, HUnit >= 1.2 && < 1.3
|
||||||
, hspec >= 1.3
|
, hspec >= 1.3
|
||||||
, containers
|
, containers
|
||||||
|
|||||||
188
yesod-static/Yesod/EmbeddedStatic.hs
Normal file
188
yesod-static/Yesod/EmbeddedStatic.hs
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
-- | A subsite which serves static content which is embedded at compile time.
|
||||||
|
--
|
||||||
|
-- At compile time, you supply a list of files, directories, processing functions (like javascript
|
||||||
|
-- minification), and even custom content generators. You can also specify the specific relative
|
||||||
|
-- locations within the static subsite where these resources should appear. The 'mkEmbeddedStatic'
|
||||||
|
-- function then computes the resources and embeds them directly into the executable at
|
||||||
|
-- compile time, so that the original files do not need to be distributed along with
|
||||||
|
-- the executable. The content is also compressed and hashed at compile time, so that
|
||||||
|
-- during runtime the compressed content can be sent directly on the wire with the appropriate
|
||||||
|
-- HTTP header. The precomputed hash is used for an ETag so the client does not redownload
|
||||||
|
-- the content multiple times. There is also a development mode which does not embed the
|
||||||
|
-- contents but recomputes it on every request. A simple example using an embedded static
|
||||||
|
-- subsite is
|
||||||
|
-- <https://github.com/yesodweb/yesod/blob/master/yesod-static/sample-embed.hs static-embed.hs>.
|
||||||
|
--
|
||||||
|
-- To add this to a scaffolded project, replace the code in @Settings/StaticFiles.hs@
|
||||||
|
-- with a call to 'mkEmbeddedStatic' with the list of all your generators, use the type
|
||||||
|
-- 'EmbeddedStatic' in your site datatype for @getStatic@, update the route for @/static@ to
|
||||||
|
-- use the type 'EmbeddedStatic', use 'embedStaticContent' for 'addStaticContent' in
|
||||||
|
-- @Foundation.hs@, use the routes generated by 'mkEmbeddedStatic' and exported by
|
||||||
|
-- @Settings/StaticFiles.hs@ to link to your static content, and finally update
|
||||||
|
-- @Application.hs@ use the variable binding created by 'mkEmbeddedStatic' which
|
||||||
|
-- contains the created 'EmbeddedStatic'.
|
||||||
|
--
|
||||||
|
-- It is recommended that you serve static resources from a separate domain to save time
|
||||||
|
-- on transmitting cookies. You can use 'urlRenderOverride' to do so, by redirecting
|
||||||
|
-- routes to this subsite to a different domain (but the same path) and then pointing the
|
||||||
|
-- alternative domain to this server. In addition, you might consider using a reverse
|
||||||
|
-- proxy like varnish or squid to cache the static content, but the embedded content in
|
||||||
|
-- this subsite is cached and served directly from memory so is already quite fast.
|
||||||
|
module Yesod.EmbeddedStatic (
|
||||||
|
-- * Subsite
|
||||||
|
EmbeddedStatic
|
||||||
|
, embeddedResourceR
|
||||||
|
, mkEmbeddedStatic
|
||||||
|
, embedStaticContent
|
||||||
|
|
||||||
|
-- * Generators
|
||||||
|
, module Yesod.EmbeddedStatic.Generators
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Applicative ((<$>))
|
||||||
|
import Data.IORef
|
||||||
|
import Data.Maybe (catMaybes)
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Network.HTTP.Types.Status (status404)
|
||||||
|
import Network.Wai (responseLBS, pathInfo)
|
||||||
|
import Network.Wai.Application.Static (staticApp)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
import Yesod.Core
|
||||||
|
( HandlerT
|
||||||
|
, Yesod(..)
|
||||||
|
, YesodSubDispatch(..)
|
||||||
|
)
|
||||||
|
import Yesod.Core.Types
|
||||||
|
( YesodSubRunnerEnv(..)
|
||||||
|
, YesodRunnerEnv(..)
|
||||||
|
)
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import qualified WaiAppStatic.Storage.Embedded as Static
|
||||||
|
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
import Yesod.EmbeddedStatic.Internal
|
||||||
|
import Yesod.EmbeddedStatic.Generators
|
||||||
|
|
||||||
|
-- Haddock doesn't support associated types in instances yet so we can't
|
||||||
|
-- export EmbeddedResourceR directly.
|
||||||
|
|
||||||
|
-- | Construct a route to an embedded resource.
|
||||||
|
embeddedResourceR :: [T.Text] -> [(T.Text, T.Text)] -> Route EmbeddedStatic
|
||||||
|
embeddedResourceR = EmbeddedResourceR
|
||||||
|
|
||||||
|
instance Yesod master => YesodSubDispatch EmbeddedStatic (HandlerT master IO) where
|
||||||
|
yesodSubDispatch YesodSubRunnerEnv {..} req = resp
|
||||||
|
where
|
||||||
|
master = yreSite ysreParentEnv
|
||||||
|
site = ysreGetSub master
|
||||||
|
resp = case pathInfo req of
|
||||||
|
("res":_) -> stApp site req
|
||||||
|
("widget":_) -> staticApp (widgetSettings site) req
|
||||||
|
#if MIN_VERSION_wai(3,0,0)
|
||||||
|
_ -> ($ responseLBS status404 [] "Not Found")
|
||||||
|
#else
|
||||||
|
_ -> return $ responseLBS status404 [] "Not Found"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
-- | Create the haskell variable for the link to the entry
|
||||||
|
mkRoute :: ComputedEntry -> Q [Dec]
|
||||||
|
mkRoute (ComputedEntry { cHaskellName = Nothing }) = return []
|
||||||
|
mkRoute (c@ComputedEntry { cHaskellName = Just name }) = do
|
||||||
|
routeType <- [t| Route EmbeddedStatic |]
|
||||||
|
link <- [| $(cLink c) |]
|
||||||
|
return [ SigD name routeType
|
||||||
|
, ValD (VarP name) (NormalB link) []
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Creates an 'EmbeddedStatic' by running, at compile time, a list of generators.
|
||||||
|
-- Each generator produces a list of entries to embed into the executable.
|
||||||
|
--
|
||||||
|
-- This template haskell splice creates a variable binding holding the resulting
|
||||||
|
-- 'EmbeddedStatic' and in addition creates variable bindings for all the routes
|
||||||
|
-- produced by the generators. For example, if a directory called static has
|
||||||
|
-- the following contents:
|
||||||
|
--
|
||||||
|
-- * js/jquery.js
|
||||||
|
--
|
||||||
|
-- * css/bootstrap.css
|
||||||
|
--
|
||||||
|
-- * img/logo.png
|
||||||
|
--
|
||||||
|
-- then a call to
|
||||||
|
--
|
||||||
|
-- > #ifdef DEVELOPMENT
|
||||||
|
-- > #define DEV_BOOL True
|
||||||
|
-- > #else
|
||||||
|
-- > #define DEV_BOOL False
|
||||||
|
-- > #endif
|
||||||
|
-- > mkEmbeddedStatic DEV_BOOL "myStatic" [embedDir "static"]
|
||||||
|
--
|
||||||
|
-- will produce variables
|
||||||
|
--
|
||||||
|
-- > myStatic :: EmbeddedStatic
|
||||||
|
-- > js_jquery_js :: Route EmbeddedStatic
|
||||||
|
-- > css_bootstrap_css :: Route EmbeddedStatic
|
||||||
|
-- > img_logo_png :: Route EmbeddedStatic
|
||||||
|
mkEmbeddedStatic :: Bool -- ^ development?
|
||||||
|
-> String -- ^ variable name for the created 'EmbeddedStatic'
|
||||||
|
-> [Generator] -- ^ the generators (see "Yesod.EmbeddedStatic.Generators")
|
||||||
|
-> Q [Dec]
|
||||||
|
mkEmbeddedStatic dev esName gen = do
|
||||||
|
entries <- concat <$> sequence gen
|
||||||
|
computed <- runIO $ mapM (if dev then devEmbed else prodEmbed) entries
|
||||||
|
|
||||||
|
let settings = Static.mkSettings $ return $ map cStEntry computed
|
||||||
|
devExtra = listE $ catMaybes $ map ebDevelExtraFiles entries
|
||||||
|
ioRef = [| unsafePerformIO $ newIORef M.empty |]
|
||||||
|
|
||||||
|
-- build the embedded static
|
||||||
|
esType <- [t| EmbeddedStatic |]
|
||||||
|
esCreate <- if dev
|
||||||
|
then [| EmbeddedStatic (develApp $settings $devExtra) $ioRef |]
|
||||||
|
else [| EmbeddedStatic (staticApp $! $settings) $ioRef |]
|
||||||
|
let es = [ SigD (mkName esName) esType
|
||||||
|
, ValD (VarP $ mkName esName) (NormalB esCreate) []
|
||||||
|
]
|
||||||
|
|
||||||
|
routes <- mapM mkRoute computed
|
||||||
|
|
||||||
|
return $ es ++ concat routes
|
||||||
|
|
||||||
|
-- | Use this for 'addStaticContent' to have the widget static content be served by
|
||||||
|
-- the embedded static subsite. For example,
|
||||||
|
--
|
||||||
|
-- > import Yesod
|
||||||
|
-- > import Yesod.EmbeddedStatic
|
||||||
|
-- > import Text.Jasmine (minifym)
|
||||||
|
-- >
|
||||||
|
-- > data MySite = { ..., getStatic :: EmbeddedStatic, ... }
|
||||||
|
-- >
|
||||||
|
-- > mkYesod "MySite" [parseRoutes|
|
||||||
|
-- > ...
|
||||||
|
-- > /static StaticR EmbeddedStatic getStatic
|
||||||
|
-- > ...
|
||||||
|
-- > |]
|
||||||
|
-- >
|
||||||
|
-- > instance Yesod MySite where
|
||||||
|
-- > ...
|
||||||
|
-- > addStaticContent = embedStaticContent getStatic StaticR mini
|
||||||
|
-- > where mini = if development then Right else minifym
|
||||||
|
-- > ...
|
||||||
|
embedStaticContent :: Yesod site
|
||||||
|
=> (site -> EmbeddedStatic) -- ^ How to retrieve the embedded static subsite from your site
|
||||||
|
-> (Route EmbeddedStatic -> Route site) -- ^ how to convert an embedded static route
|
||||||
|
-> (BL.ByteString -> Either a BL.ByteString) -- ^ javascript minifier
|
||||||
|
-> AddStaticContent site
|
||||||
|
embedStaticContent = staticContentHelper
|
||||||
80
yesod-static/Yesod/EmbeddedStatic/Css/AbsoluteUrl.hs
Normal file
80
yesod-static/Yesod/EmbeddedStatic/Css/AbsoluteUrl.hs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- | Manipulate CSS urls.
|
||||||
|
--
|
||||||
|
-- * Make relative urls absolute (useful when combining assets)
|
||||||
|
module Yesod.EmbeddedStatic.Css.AbsoluteUrl (
|
||||||
|
-- * Absolute urls
|
||||||
|
absoluteUrls
|
||||||
|
, absoluteUrlsAt
|
||||||
|
, absoluteUrlsWith
|
||||||
|
, absCssUrlsFileProd
|
||||||
|
, absCssUrlsProd
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Prelude hiding (FilePath)
|
||||||
|
import Yesod.EmbeddedStatic.Generators
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.IO as T
|
||||||
|
import qualified Data.Text.Lazy.Encoding as TL
|
||||||
|
import Control.Monad ((>=>))
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Filesystem.Path.CurrentOS ((</>), collapse, FilePath, fromText, toText, encodeString, decodeString)
|
||||||
|
|
||||||
|
import Yesod.EmbeddedStatic.Css.Util
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Generator
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Anchors relative CSS image urls
|
||||||
|
absCssUrlsFileProd :: FilePath -- ^ Anchor relative urls to here
|
||||||
|
-> FilePath
|
||||||
|
-> IO BL.ByteString
|
||||||
|
absCssUrlsFileProd dir file = do
|
||||||
|
contents <- T.readFile (encodeString file)
|
||||||
|
return $ TL.encodeUtf8 $ absCssUrlsProd dir contents
|
||||||
|
|
||||||
|
absCssUrlsProd :: FilePath -- ^ Anchor relative urls to here
|
||||||
|
-> T.Text
|
||||||
|
-> TL.Text
|
||||||
|
absCssUrlsProd dir contents =
|
||||||
|
let css = either error id $ parseCssUrls contents
|
||||||
|
in renderCssWith toAbsoluteUrl css
|
||||||
|
where
|
||||||
|
toAbsoluteUrl (UrlReference rel) = T.concat
|
||||||
|
[ "url('/"
|
||||||
|
, (either id id $ toText $ collapse $ dir </> fromText rel)
|
||||||
|
, "')"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
-- | Equivalent to passing the same string twice to 'absoluteUrlsAt'.
|
||||||
|
absoluteUrls :: FilePath -> Generator
|
||||||
|
absoluteUrls f = absoluteUrlsAt (encodeString f) f
|
||||||
|
|
||||||
|
-- | Equivalent to passing @return@ to 'absoluteUrlsWith'.
|
||||||
|
absoluteUrlsAt :: Location -> FilePath -> Generator
|
||||||
|
absoluteUrlsAt loc f = absoluteUrlsWith loc f Nothing
|
||||||
|
|
||||||
|
-- | Automatically make relative urls absolute
|
||||||
|
--
|
||||||
|
-- During development, leave CSS as is.
|
||||||
|
--
|
||||||
|
-- When CSS is organized into a directory structure, it will work properly for individual requests for each file.
|
||||||
|
-- During production, we want to combine and minify CSS as much as possible.
|
||||||
|
-- The combination process combines files from different directories, messing up relative urls.
|
||||||
|
-- This pre-processor makes relative urls absolute
|
||||||
|
absoluteUrlsWith ::
|
||||||
|
Location -- ^ The location the CSS file should appear in the static subsite
|
||||||
|
-> FilePath -- ^ Path to the CSS file.
|
||||||
|
-> Maybe (CssGeneration -> IO BL.ByteString) -- ^ Another filter function run after this one (for example @return . yuiCSS . cssContent@) or other CSS filter that runs after this filter.
|
||||||
|
-> Generator
|
||||||
|
absoluteUrlsWith loc file mpostFilter =
|
||||||
|
return [ cssProductionFilter (absCssUrlsFileProd (decodeString loc) >=> postFilter . mkCssGeneration loc file) loc file
|
||||||
|
]
|
||||||
|
where
|
||||||
|
postFilter = fromMaybe (return . cssContent) mpostFilter
|
||||||
196
yesod-static/Yesod/EmbeddedStatic/Css/Util.hs
Normal file
196
yesod-static/Yesod/EmbeddedStatic/Css/Util.hs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, TupleSections, GeneralizedNewtypeDeriving #-}
|
||||||
|
module Yesod.EmbeddedStatic.Css.Util where
|
||||||
|
|
||||||
|
import Prelude hiding (FilePath)
|
||||||
|
import Control.Applicative
|
||||||
|
import Control.Monad (void, foldM)
|
||||||
|
import Data.Hashable (Hashable)
|
||||||
|
import Data.Monoid
|
||||||
|
import Network.Mime (MimeType, defaultMimeLookup)
|
||||||
|
import Filesystem.Path.CurrentOS (FilePath, directory, (</>), dropExtension, filename, toText, decodeString, encodeString, fromText, absolute)
|
||||||
|
import Text.CSS.Parse (parseBlocks)
|
||||||
|
import Language.Haskell.TH (litE, stringL)
|
||||||
|
import Text.CSS.Render (renderBlocks)
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
import Yesod.EmbeddedStatic (pathToName)
|
||||||
|
import Data.Default (def)
|
||||||
|
|
||||||
|
import qualified Blaze.ByteString.Builder as B
|
||||||
|
import qualified Blaze.ByteString.Builder.Char.Utf8 as B
|
||||||
|
import qualified Data.Attoparsec.Text as P
|
||||||
|
import qualified Data.Attoparsec.ByteString.Lazy as PBL
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.ByteString.Base64 as B64
|
||||||
|
import qualified Data.HashMap.Lazy as M
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Data.Text.IO as T
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.Lazy.Builder as TL
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Loading CSS
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | In the parsed CSS, this will be an image reference that we want to replace.
|
||||||
|
-- the contents will be the filepath.
|
||||||
|
newtype UrlReference = UrlReference T.Text
|
||||||
|
deriving (Show, Eq, Hashable, Ord)
|
||||||
|
|
||||||
|
type EithUrl = (T.Text, Either T.Text UrlReference)
|
||||||
|
|
||||||
|
-- | The parsed CSS
|
||||||
|
type Css = [(T.Text, [EithUrl])]
|
||||||
|
|
||||||
|
-- | Parse the filename out of url('filename')
|
||||||
|
parseUrl :: P.Parser T.Text
|
||||||
|
parseUrl = do
|
||||||
|
P.skipSpace
|
||||||
|
void $ P.string "url('"
|
||||||
|
P.takeTill (== '\'')
|
||||||
|
|
||||||
|
checkForUrl :: T.Text -> T.Text -> EithUrl
|
||||||
|
checkForUrl n@("background-image") v = parseBackgroundImage n v
|
||||||
|
checkForUrl n@("src") v = parseBackgroundImage n v
|
||||||
|
checkForUrl n v = (n, Left v)
|
||||||
|
|
||||||
|
-- | Check if a given CSS attribute is a background image referencing a local file
|
||||||
|
checkForImage :: T.Text -> T.Text -> EithUrl
|
||||||
|
checkForImage n@("background-image") v = parseBackgroundImage n v
|
||||||
|
checkForImage n v = (n, Left v)
|
||||||
|
|
||||||
|
parseBackgroundImage :: T.Text -> T.Text -> EithUrl
|
||||||
|
parseBackgroundImage n v = (n, case P.parseOnly parseUrl v of
|
||||||
|
Left _ -> Left v -- Can't parse url
|
||||||
|
Right url -> -- maybe we should find a uri parser
|
||||||
|
if any (`T.isPrefixOf` url) ["http://", "https://", "//"] || absolute (fromText url)
|
||||||
|
then Left v
|
||||||
|
else Right $ UrlReference url)
|
||||||
|
|
||||||
|
parseCssWith :: (T.Text -> T.Text -> EithUrl) -> T.Text -> Either String Css
|
||||||
|
parseCssWith urlParser contents =
|
||||||
|
let mparsed = parseBlocks contents in
|
||||||
|
case mparsed of
|
||||||
|
Left err -> Left err
|
||||||
|
Right blocks -> Right [ (t, map (uncurry urlParser) b) | (t,b) <- blocks ]
|
||||||
|
|
||||||
|
parseCssUrls :: T.Text -> Either String Css
|
||||||
|
parseCssUrls = parseCssWith checkForUrl
|
||||||
|
|
||||||
|
parseCssFileWith :: (T.Text -> T.Text -> EithUrl) -> FilePath -> IO Css
|
||||||
|
parseCssFileWith urlParser fp = do
|
||||||
|
mparsed <- parseCssWith urlParser <$> T.readFile (encodeString fp)
|
||||||
|
case mparsed of
|
||||||
|
Left err -> fail $ "Unable to parse " ++ encodeString fp ++ ": " ++ err
|
||||||
|
Right css -> return css
|
||||||
|
|
||||||
|
parseCssFileUrls :: FilePath -> IO Css
|
||||||
|
parseCssFileUrls = parseCssFileWith checkForUrl
|
||||||
|
|
||||||
|
renderCssWith :: (UrlReference -> T.Text) -> Css -> TL.Text
|
||||||
|
renderCssWith urlRenderer css =
|
||||||
|
TL.toLazyText $ renderBlocks [(n, map render block) | (n,block) <- css]
|
||||||
|
where
|
||||||
|
render (n, Left b) = (n, b)
|
||||||
|
render (n, Right f) = (n, urlRenderer f)
|
||||||
|
|
||||||
|
-- | Load an image map from the images in the CSS
|
||||||
|
loadImages :: FilePath -> Css -> (FilePath -> IO (Maybe a)) -> IO (M.HashMap UrlReference a)
|
||||||
|
loadImages dir css loadImage = foldM load M.empty $ concat [map snd block | (_,block) <- css]
|
||||||
|
where
|
||||||
|
load imap (Left _) = return imap
|
||||||
|
load imap (Right f) | f `M.member` imap = return imap
|
||||||
|
load imap (Right f@(UrlReference path)) = do
|
||||||
|
img <- loadImage (dir </> fromText path)
|
||||||
|
return $ maybe imap (\i -> M.insert f i imap) img
|
||||||
|
|
||||||
|
|
||||||
|
-- | If you tack on additional CSS post-processing filters, they use this as an argument.
|
||||||
|
data CssGeneration = CssGeneration {
|
||||||
|
cssContent :: BL.ByteString
|
||||||
|
, cssStaticLocation :: Location
|
||||||
|
, cssFileLocation :: FilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
mkCssGeneration :: Location -> FilePath -> BL.ByteString -> CssGeneration
|
||||||
|
mkCssGeneration loc file content =
|
||||||
|
CssGeneration { cssContent = content
|
||||||
|
, cssStaticLocation = loc
|
||||||
|
, cssFileLocation = file
|
||||||
|
}
|
||||||
|
|
||||||
|
cssProductionFilter ::
|
||||||
|
(FilePath -> IO BL.ByteString) -- ^ a filter to be run on production
|
||||||
|
-> Location -- ^ The location the CSS file should appear in the static subsite
|
||||||
|
-> FilePath -- ^ Path to the CSS file.
|
||||||
|
-> Entry
|
||||||
|
cssProductionFilter prodFilter loc file =
|
||||||
|
def { ebHaskellName = Just $ pathToName loc
|
||||||
|
, ebLocation = loc
|
||||||
|
, ebMimeType = "text/css"
|
||||||
|
, ebProductionContent = prodFilter file
|
||||||
|
, ebDevelReload = [| develPassThrough $(litE (stringL loc)) $(litE (stringL $ encodeString file)) |]
|
||||||
|
, ebDevelExtraFiles = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
cssProductionImageFilter :: (FilePath -> IO BL.ByteString) -> Location -> FilePath -> Entry
|
||||||
|
cssProductionImageFilter prodFilter loc file =
|
||||||
|
(cssProductionFilter prodFilter loc file)
|
||||||
|
{ ebDevelReload = [| develBgImgB64 $(litE (stringL loc)) $(litE (stringL $ encodeString file)) |]
|
||||||
|
, ebDevelExtraFiles = Just [| develExtraFiles $(litE (stringL loc)) |]
|
||||||
|
}
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Helpers for the generators
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- For development, all we need to do is update the background-image url to base64 encode it.
|
||||||
|
-- We want to preserve the formatting (whitespace+newlines) during development so we do not parse
|
||||||
|
-- using css-parse. Instead we write a simple custom parser.
|
||||||
|
|
||||||
|
parseBackground :: Location -> FilePath -> PBL.Parser B.Builder
|
||||||
|
parseBackground loc file = do
|
||||||
|
void $ PBL.string "background-image"
|
||||||
|
s1 <- PBL.takeWhile (\x -> x == 32 || x == 9) -- space or tab
|
||||||
|
void $ PBL.word8 58 -- colon
|
||||||
|
s2 <- PBL.takeWhile (\x -> x == 32 || x == 9) -- space or tab
|
||||||
|
void $ PBL.string "url('"
|
||||||
|
url <- PBL.takeWhile (/= 39) -- single quote
|
||||||
|
void $ PBL.string "')"
|
||||||
|
|
||||||
|
let b64 = B64.encode $ T.encodeUtf8 (either id id $ toText (directory file)) <> url
|
||||||
|
newUrl = B.fromString (encodeString $ filename $ decodeString loc) <> B.fromString "/" <> B.fromByteString b64
|
||||||
|
|
||||||
|
return $ B.fromByteString "background-image"
|
||||||
|
<> B.fromByteString s1
|
||||||
|
<> B.fromByteString ":"
|
||||||
|
<> B.fromByteString s2
|
||||||
|
<> B.fromByteString "url('"
|
||||||
|
<> newUrl
|
||||||
|
<> B.fromByteString "')"
|
||||||
|
|
||||||
|
parseDev :: Location -> FilePath -> B.Builder -> PBL.Parser B.Builder
|
||||||
|
parseDev loc file b = do
|
||||||
|
b' <- parseBackground loc file <|> (B.fromWord8 <$> PBL.anyWord8)
|
||||||
|
(PBL.endOfInput *> (pure $! b <> b')) <|> (parseDev loc file $! b <> b')
|
||||||
|
|
||||||
|
develPassThrough :: Location -> FilePath -> IO BL.ByteString
|
||||||
|
develPassThrough _ = BL.readFile . encodeString
|
||||||
|
|
||||||
|
-- | Create the CSS during development
|
||||||
|
develBgImgB64 :: Location -> FilePath -> IO BL.ByteString
|
||||||
|
develBgImgB64 loc file = do
|
||||||
|
ct <- BL.readFile $ encodeString file
|
||||||
|
case PBL.eitherResult $ PBL.parse (parseDev loc file mempty) ct of
|
||||||
|
Left err -> error err
|
||||||
|
Right b -> return $ B.toLazyByteString b
|
||||||
|
|
||||||
|
-- | Serve the extra image files during development
|
||||||
|
develExtraFiles :: Location -> [T.Text] -> IO (Maybe (MimeType, BL.ByteString))
|
||||||
|
develExtraFiles loc parts =
|
||||||
|
case reverse parts of
|
||||||
|
(file:dir) | T.pack loc == T.intercalate "/" (reverse dir) -> do
|
||||||
|
let file' = T.decodeUtf8 $ B64.decodeLenient $ T.encodeUtf8 $ either id id $ toText $ dropExtension $ fromText file
|
||||||
|
ct <- BL.readFile $ T.unpack file'
|
||||||
|
return $ Just (defaultMimeLookup file', ct)
|
||||||
|
_ -> return Nothing
|
||||||
329
yesod-static/Yesod/EmbeddedStatic/Generators.hs
Normal file
329
yesod-static/Yesod/EmbeddedStatic/Generators.hs
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, ScopedTypeVariables #-}
|
||||||
|
-- | A generator is executed at compile time to load a list of entries
|
||||||
|
-- to embed into the subsite. This module contains several basic generators,
|
||||||
|
-- but the design of generators and entries is such that it is straightforward
|
||||||
|
-- to make custom generators for your own specific purposes, see <#g:4 this section>.
|
||||||
|
module Yesod.EmbeddedStatic.Generators (
|
||||||
|
-- * Generators
|
||||||
|
Location
|
||||||
|
, embedFile
|
||||||
|
, embedFileAt
|
||||||
|
, embedDir
|
||||||
|
, embedDirAt
|
||||||
|
, concatFiles
|
||||||
|
, concatFilesWith
|
||||||
|
|
||||||
|
-- * Compression options for 'concatFilesWith'
|
||||||
|
, jasmine
|
||||||
|
, uglifyJs
|
||||||
|
, yuiJavascript
|
||||||
|
, yuiCSS
|
||||||
|
, closureJs
|
||||||
|
, compressTool
|
||||||
|
, tryCompressTools
|
||||||
|
|
||||||
|
-- * Util
|
||||||
|
, pathToName
|
||||||
|
|
||||||
|
-- * Custom Generators
|
||||||
|
|
||||||
|
-- $example
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Applicative ((<$>), (<*>))
|
||||||
|
import Control.Exception (try, SomeException)
|
||||||
|
import Control.Monad (forM, when)
|
||||||
|
import Control.Monad.Trans.Resource (runResourceT)
|
||||||
|
import Data.Char (isDigit, isLower)
|
||||||
|
import Data.Conduit (($$))
|
||||||
|
import Data.Default (def)
|
||||||
|
import Data.Maybe (isNothing)
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Network.Mime (defaultMimeLookup)
|
||||||
|
import System.Directory (doesDirectoryExist, getDirectoryContents, findExecutable)
|
||||||
|
import System.FilePath ((</>))
|
||||||
|
import Text.Jasmine (minifym)
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
import Data.Conduit.Binary (sourceHandle)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified System.Process as Proc
|
||||||
|
import System.Exit (ExitCode (ExitSuccess))
|
||||||
|
import Control.Concurrent.Async (Concurrently (..))
|
||||||
|
import System.IO (hClose)
|
||||||
|
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
|
||||||
|
-- | Embed a single file. Equivalent to passing the same string twice to 'embedFileAt'.
|
||||||
|
embedFile :: FilePath -> Generator
|
||||||
|
embedFile f = embedFileAt f f
|
||||||
|
|
||||||
|
-- | Embed a single file at a given location within the static subsite and generate a
|
||||||
|
-- route variable based on the location via 'pathToName'. The @FilePath@ must be a relative
|
||||||
|
-- path to the directory in which you run @cabal build@. During development, the file located
|
||||||
|
-- at this filepath will be reloaded on every request. When compiling for production, the contents
|
||||||
|
-- of the file will be embedded into the executable and so the file does not need to be
|
||||||
|
-- distributed along with the executable.
|
||||||
|
embedFileAt :: Location -> FilePath -> Generator
|
||||||
|
embedFileAt loc f = do
|
||||||
|
let mime = defaultMimeLookup $ T.pack f
|
||||||
|
let entry = def {
|
||||||
|
ebHaskellName = Just $ pathToName loc
|
||||||
|
, ebLocation = loc
|
||||||
|
, ebMimeType = mime
|
||||||
|
, ebProductionContent = BL.readFile f
|
||||||
|
, ebDevelReload = [| BL.readFile $(litE $ stringL f) |]
|
||||||
|
}
|
||||||
|
return [entry]
|
||||||
|
|
||||||
|
-- | List all files recursively in a directory
|
||||||
|
getRecursiveContents :: Location -- ^ The directory to search
|
||||||
|
-> FilePath -- ^ The prefix to add to the filenames
|
||||||
|
-> IO [(Location,FilePath)]
|
||||||
|
getRecursiveContents prefix topdir = do
|
||||||
|
names <- getDirectoryContents topdir
|
||||||
|
let properNames = filter (`notElem` [".", ".."]) names
|
||||||
|
paths <- forM properNames $ \name -> do
|
||||||
|
let path = topdir </> name
|
||||||
|
let loc = if null prefix then name else prefix ++ "/" ++ name
|
||||||
|
isDirectory <- doesDirectoryExist path
|
||||||
|
if isDirectory
|
||||||
|
then getRecursiveContents loc path
|
||||||
|
else return [(loc, path)]
|
||||||
|
return (concat paths)
|
||||||
|
|
||||||
|
-- | Embed all files in a directory into the static subsite.
|
||||||
|
--
|
||||||
|
-- Equivalent to passing the empty string as the location to 'embedDirAt',
|
||||||
|
-- so the directory path itself is not part of the resource locations (and so
|
||||||
|
-- also not part of the generated route variable names).
|
||||||
|
embedDir :: FilePath -> Generator
|
||||||
|
embedDir = embedDirAt ""
|
||||||
|
|
||||||
|
-- | Embed all files in a directory to a given location within the static subsite.
|
||||||
|
--
|
||||||
|
-- The directory tree rooted at the 'FilePath' (which must be relative to the directory in
|
||||||
|
-- which you run @cabal build@) is embedded into the static subsite at the given
|
||||||
|
-- location. Also, route variables will be created based on the final location
|
||||||
|
-- of each file. For example, if a directory \"static\" contains the files
|
||||||
|
--
|
||||||
|
-- * css/bootstrap.css
|
||||||
|
--
|
||||||
|
-- * js/jquery.js
|
||||||
|
--
|
||||||
|
-- * js/bootstrap.js
|
||||||
|
--
|
||||||
|
-- then @embedDirAt \"somefolder\" \"static\"@ will
|
||||||
|
--
|
||||||
|
-- * Make the file @static\/css\/bootstrap.css@ available at the location
|
||||||
|
-- @somefolder\/css\/bootstrap.css@ within the static subsite and similarly
|
||||||
|
-- for the other two files.
|
||||||
|
--
|
||||||
|
-- * Create variables @somefolder_css_bootstrap_css@, @somefolder_js_jquery_js@,
|
||||||
|
-- @somefolder_js_bootstrap_js@ all of type @Route EmbeddedStatic@.
|
||||||
|
--
|
||||||
|
-- * During development, the files will be reloaded on every request. During
|
||||||
|
-- production, the contents of all files will be embedded into the executable.
|
||||||
|
--
|
||||||
|
-- * During development, files that are added to the directory while the server
|
||||||
|
-- is running will not be detected. You need to recompile the module which
|
||||||
|
-- contains the call to @mkEmbeddedStatic@. This will also generate new route
|
||||||
|
-- variables for the new files.
|
||||||
|
embedDirAt :: Location -> FilePath -> Generator
|
||||||
|
embedDirAt loc dir = do
|
||||||
|
files <- runIO $ getRecursiveContents loc dir
|
||||||
|
concat <$> mapM (uncurry embedFileAt) files
|
||||||
|
|
||||||
|
-- | Concatinate a list of files and embed it at the location. Equivalent to passing @return@ to
|
||||||
|
-- 'concatFilesWith'.
|
||||||
|
concatFiles :: Location -> [FilePath] -> Generator
|
||||||
|
concatFiles loc files = concatFilesWith loc return files
|
||||||
|
|
||||||
|
-- | Concatinate a list of files into a single 'BL.ByteString', run the resulting content through the given
|
||||||
|
-- function, embed it at the given location, and create a haskell variable name for the route based on
|
||||||
|
-- the location.
|
||||||
|
--
|
||||||
|
-- The processing function is only run when compiling for production, and the processing function is
|
||||||
|
-- executed at compile time. During development, on every request the files listed are reloaded,
|
||||||
|
-- concatenated, and served as a single resource at the given location without being processed.
|
||||||
|
concatFilesWith :: Location -> (BL.ByteString -> IO BL.ByteString) -> [FilePath] -> Generator
|
||||||
|
concatFilesWith loc process files = do
|
||||||
|
let load = do putStrLn $ "Creating " ++ loc
|
||||||
|
BL.concat <$> mapM BL.readFile files >>= process
|
||||||
|
expFiles = listE $ map (litE . stringL) files
|
||||||
|
expCt = [| BL.concat <$> mapM BL.readFile $expFiles |]
|
||||||
|
mime = defaultMimeLookup $ T.pack loc
|
||||||
|
return [def { ebHaskellName = Just $ pathToName loc
|
||||||
|
, ebLocation = loc
|
||||||
|
, ebMimeType = mime
|
||||||
|
, ebProductionContent = load
|
||||||
|
, ebDevelReload = expCt
|
||||||
|
}]
|
||||||
|
|
||||||
|
-- | Convienient rexport of 'minifym' with a type signature to work with 'concatFilesWith'.
|
||||||
|
jasmine :: BL.ByteString -> IO BL.ByteString
|
||||||
|
jasmine ct = return $ either (const ct) id $ minifym ct
|
||||||
|
|
||||||
|
-- | Use <https://github.com/mishoo/UglifyJS2 UglifyJS2> to compress javascript.
|
||||||
|
-- Assumes @uglifyjs@ is located in the path and uses options @[\"-m\", \"-c\"]@
|
||||||
|
-- to both mangle and compress and the option \"-\" to cause uglifyjs to read from
|
||||||
|
-- standard input.
|
||||||
|
uglifyJs :: BL.ByteString -> IO BL.ByteString
|
||||||
|
uglifyJs = compressTool "uglifyjs" ["-m", "-c", "-"]
|
||||||
|
|
||||||
|
-- | Use <http://yui.github.io/yuicompressor/ YUI Compressor> to compress javascript.
|
||||||
|
-- Assumes a script @yuicompressor@ is located in the path. If not, you can still
|
||||||
|
-- use something like
|
||||||
|
--
|
||||||
|
-- > compressTool "java" ["-jar", "/path/to/yuicompressor.jar", "--type", "js"]
|
||||||
|
yuiJavascript :: BL.ByteString -> IO BL.ByteString
|
||||||
|
yuiJavascript = compressTool "yuicompressor" ["--type", "js"]
|
||||||
|
|
||||||
|
-- | Use <http://yui.github.io/yuicompressor/ YUI Compressor> to compress CSS.
|
||||||
|
-- Assumes a script @yuicompressor@ is located in the path.
|
||||||
|
yuiCSS :: BL.ByteString -> IO BL.ByteString
|
||||||
|
yuiCSS = compressTool "yuicompressor" ["--type", "css"]
|
||||||
|
|
||||||
|
-- | Use <https://developers.google.com/closure/compiler/ Closure> to compress
|
||||||
|
-- javascript using the default options. Assumes a script @closure@ is located in
|
||||||
|
-- the path. If not, you can still run using
|
||||||
|
--
|
||||||
|
-- > compressTool "java" ["-jar", "/path/to/compiler.jar"]
|
||||||
|
closureJs :: BL.ByteString -> IO BL.ByteString
|
||||||
|
closureJs = compressTool "closure" []
|
||||||
|
|
||||||
|
-- | Helper to convert a process into a compression function. The process
|
||||||
|
-- should be set up to take input from standard input and write to standard output.
|
||||||
|
compressTool :: FilePath -- ^ program
|
||||||
|
-> [String] -- ^ options
|
||||||
|
-> BL.ByteString -> IO BL.ByteString
|
||||||
|
compressTool f opts ct = do
|
||||||
|
mpath <- findExecutable f
|
||||||
|
when (isNothing mpath) $
|
||||||
|
fail $ "Unable to find " ++ f
|
||||||
|
let p = (Proc.proc f opts)
|
||||||
|
{ Proc.std_in = Proc.CreatePipe
|
||||||
|
, Proc.std_out = Proc.CreatePipe
|
||||||
|
}
|
||||||
|
(Just hin, Just hout, _, ph) <- Proc.createProcess p
|
||||||
|
(compressed, (), code) <- runConcurrently $ (,,)
|
||||||
|
<$> Concurrently (sourceHandle hout $$ C.consume)
|
||||||
|
<*> Concurrently (BL.hPut hin ct >> hClose hin)
|
||||||
|
<*> Concurrently (Proc.waitForProcess ph)
|
||||||
|
if code == ExitSuccess
|
||||||
|
then do
|
||||||
|
putStrLn $ "Compressed successfully with " ++ f
|
||||||
|
return $ BL.fromChunks compressed
|
||||||
|
else error $ "compressTool: compression failed with " ++ f
|
||||||
|
|
||||||
|
|
||||||
|
-- | Try a list of processing functions (like the compressions above) one by one until
|
||||||
|
-- one succeeds (does not raise an exception). Once a processing function succeeds,
|
||||||
|
-- none of the remaining functions are used. If none succeeds, the input is just
|
||||||
|
-- returned unprocessed. This is helpful if you are distributing
|
||||||
|
-- code on hackage and do not know what compressors the user will have installed. You
|
||||||
|
-- can list several and they will be tried in order until one succeeds.
|
||||||
|
tryCompressTools :: [BL.ByteString -> IO BL.ByteString] -> BL.ByteString -> IO BL.ByteString
|
||||||
|
tryCompressTools [] x = return x
|
||||||
|
tryCompressTools (p:ps) x = do
|
||||||
|
mres <- try $ p x
|
||||||
|
case mres of
|
||||||
|
Left (err :: SomeException) -> do
|
||||||
|
putStrLn $ show err
|
||||||
|
tryCompressTools ps x
|
||||||
|
Right res -> return res
|
||||||
|
|
||||||
|
-- | Clean up a path to make it a valid haskell name by replacing all non-letters
|
||||||
|
-- and non-numbers by underscores. In addition, if the path starts with a capital
|
||||||
|
-- letter or number add an initial underscore.
|
||||||
|
pathToName :: FilePath -> Name
|
||||||
|
pathToName f = routeName
|
||||||
|
where
|
||||||
|
replace c
|
||||||
|
| 'A' <= c && c <= 'Z' = c
|
||||||
|
| 'a' <= c && c <= 'z' = c
|
||||||
|
| '0' <= c && c <= '9' = c
|
||||||
|
| otherwise = '_'
|
||||||
|
name = map replace f
|
||||||
|
routeName = mkName $
|
||||||
|
case () of
|
||||||
|
()
|
||||||
|
| null name -> error "null-named file"
|
||||||
|
| isDigit (head name) -> '_' : name
|
||||||
|
| isLower (head name) -> name
|
||||||
|
| otherwise -> '_' : name
|
||||||
|
|
||||||
|
|
||||||
|
-- $example
|
||||||
|
-- Here is an example of creating your own custom generator.
|
||||||
|
-- Because of template haskell stage restrictions, you must define generators in a
|
||||||
|
-- different module from where you use them. The following generator will embed a
|
||||||
|
-- JSON document that contains the compile time.
|
||||||
|
--
|
||||||
|
-- >{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
|
||||||
|
-- >module CompileTime where
|
||||||
|
-- >
|
||||||
|
-- >import Data.Aeson
|
||||||
|
-- >import Data.Default
|
||||||
|
-- >import Data.Time
|
||||||
|
-- >import Yesod.EmbeddedStatic.Generators
|
||||||
|
-- >import Yesod.EmbeddedStatic.Types
|
||||||
|
-- >import qualified Data.ByteString.Lazy as BL
|
||||||
|
-- >
|
||||||
|
-- >getTime :: IO BL.ByteString
|
||||||
|
-- >getTime = do
|
||||||
|
-- > t <- getCurrentTime
|
||||||
|
-- > return $ encode $
|
||||||
|
-- > object [ "compile_time" .= show t ]
|
||||||
|
-- >
|
||||||
|
-- >timeGenerator :: Location -> Generator
|
||||||
|
-- >timeGenerator loc =
|
||||||
|
-- > return $ [def
|
||||||
|
-- > { ebHaskellName = Just $ pathToName loc
|
||||||
|
-- > , ebLocation = loc
|
||||||
|
-- > , ebMimeType = "application/json"
|
||||||
|
-- > , ebProductionContent = getTime
|
||||||
|
-- > , ebDevelReload = [| getTime |]
|
||||||
|
-- > }]
|
||||||
|
--
|
||||||
|
-- Notice how the @getTime@ action is given as both 'ebProductionContent' and
|
||||||
|
-- 'ebDevelReload'. The result is that during development, the @getTime@ action
|
||||||
|
-- will be re-executed on every request so the time returned will be different
|
||||||
|
-- for each reload. When compiling for production, the @getTime@ action will
|
||||||
|
-- be executed once at compile time to produce the content to embed and never
|
||||||
|
-- called at runtime.
|
||||||
|
--
|
||||||
|
-- Here is a small example yesod program using this generator. Try toggling
|
||||||
|
-- the development argument to @mkEmbeddedStatic@.
|
||||||
|
--
|
||||||
|
-- >{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies #-}
|
||||||
|
-- >module Main where
|
||||||
|
-- >
|
||||||
|
-- >import Yesod
|
||||||
|
-- >import Yesod.EmbeddedStatic
|
||||||
|
-- >import CompileTime (timeGenerator)
|
||||||
|
-- >
|
||||||
|
-- >mkEmbeddedStatic True "eStatic" [timeGenerator "compile-time.json"]
|
||||||
|
-- >
|
||||||
|
-- >-- The above will generate variables
|
||||||
|
-- >-- eStatic :: EmbeddedStatic
|
||||||
|
-- >-- compile_time_json :: Route EmbeddedStatic
|
||||||
|
-- >
|
||||||
|
-- >data MyApp = MyApp { getStatic :: EmbeddedStatic }
|
||||||
|
-- >
|
||||||
|
-- >mkYesod "MyApp" [parseRoutes|
|
||||||
|
-- >/ HomeR GET
|
||||||
|
-- >/static StaticR EmbeddedStatic getStatic
|
||||||
|
-- >|]
|
||||||
|
-- >
|
||||||
|
-- >instance Yesod MyApp
|
||||||
|
-- >
|
||||||
|
-- >getHomeR :: Handler Html
|
||||||
|
-- >getHomeR = defaultLayout $ [whamlet|
|
||||||
|
-- ><h1>Hello
|
||||||
|
-- ><p>Check the
|
||||||
|
-- > <a href=@{StaticR compile_time_json}>compile time
|
||||||
|
-- >|]
|
||||||
|
-- >
|
||||||
|
-- >main :: IO ()
|
||||||
|
-- >main = warp 3000 $ MyApp eStatic
|
||||||
187
yesod-static/Yesod/EmbeddedStatic/Internal.hs
Normal file
187
yesod-static/Yesod/EmbeddedStatic/Internal.hs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
module Yesod.EmbeddedStatic.Internal (
|
||||||
|
EmbeddedStatic(..)
|
||||||
|
, Route(..)
|
||||||
|
, ComputedEntry(..)
|
||||||
|
, devEmbed
|
||||||
|
, prodEmbed
|
||||||
|
, develApp
|
||||||
|
, AddStaticContent
|
||||||
|
, staticContentHelper
|
||||||
|
, widgetSettings
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Applicative ((<$>))
|
||||||
|
import Data.IORef
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Network.HTTP.Types (Status(..), status404, status200, status304)
|
||||||
|
import Network.Mime (MimeType)
|
||||||
|
import Network.Wai
|
||||||
|
import Network.Wai.Application.Static (defaultWebAppSettings, staticApp)
|
||||||
|
import WaiAppStatic.Types
|
||||||
|
import Yesod.Core
|
||||||
|
( HandlerT
|
||||||
|
, ParseRoute(..)
|
||||||
|
, RenderRoute(..)
|
||||||
|
, Yesod(..)
|
||||||
|
, getYesod
|
||||||
|
, liftIO
|
||||||
|
)
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import qualified WaiAppStatic.Storage.Embedded as Static
|
||||||
|
|
||||||
|
import Yesod.Static (base64md5)
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
|
||||||
|
#if !MIN_VERSION_base(4,6,0)
|
||||||
|
-- copied from base
|
||||||
|
atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
|
||||||
|
atomicModifyIORef' ref f = do
|
||||||
|
b <- atomicModifyIORef ref
|
||||||
|
(\x -> let (a, b) = f x
|
||||||
|
in (a, a `seq` b))
|
||||||
|
b `seq` return b
|
||||||
|
#endif
|
||||||
|
|
||||||
|
-- | The subsite for the embedded static file server.
|
||||||
|
data EmbeddedStatic = EmbeddedStatic {
|
||||||
|
stApp :: !Application
|
||||||
|
, widgetFiles :: !(IORef (M.HashMap T.Text File))
|
||||||
|
}
|
||||||
|
|
||||||
|
instance RenderRoute EmbeddedStatic where
|
||||||
|
data Route EmbeddedStatic = EmbeddedResourceR [T.Text] [(T.Text,T.Text)]
|
||||||
|
| EmbeddedWidgetR T.Text
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
renderRoute (EmbeddedResourceR x y) = ("res":x, y)
|
||||||
|
renderRoute (EmbeddedWidgetR h) = (["widget",h], [])
|
||||||
|
instance ParseRoute EmbeddedStatic where
|
||||||
|
parseRoute (("res":x), y) = Just $ EmbeddedResourceR x y
|
||||||
|
parseRoute (["widget",h], _) = Just $ EmbeddedWidgetR h
|
||||||
|
parseRoute _ = Nothing
|
||||||
|
|
||||||
|
-- | At compile time, one of these is created for every 'Entry' created by
|
||||||
|
-- the generators. The cLink is a template haskell expression of type @Route EmbeddedStatic@.
|
||||||
|
data ComputedEntry = ComputedEntry {
|
||||||
|
cHaskellName :: Maybe Name -- ^ Optional haskell name to create a variable for the route
|
||||||
|
, cStEntry :: Static.EmbeddableEntry -- ^ The entry to be embedded into the executable
|
||||||
|
, cLink :: ExpQ -- ^ The route for this entry
|
||||||
|
}
|
||||||
|
|
||||||
|
mkStr :: String -> ExpQ
|
||||||
|
mkStr = litE . stringL
|
||||||
|
|
||||||
|
-- | Create a 'ComputedEntry' for development mode, reloading the content on every request.
|
||||||
|
devEmbed :: Entry -> IO ComputedEntry
|
||||||
|
devEmbed e = return computed
|
||||||
|
where
|
||||||
|
st = Static.EmbeddableEntry {
|
||||||
|
Static.eLocation = "res/" `T.append` T.pack (ebLocation e)
|
||||||
|
, Static.eMimeType = ebMimeType e
|
||||||
|
, Static.eContent = Right [| $(ebDevelReload e) >>= \c ->
|
||||||
|
return (T.pack (base64md5 c), c) |]
|
||||||
|
}
|
||||||
|
link = [| EmbeddedResourceR (T.splitOn (T.pack "/") $ T.pack $(mkStr $ ebLocation e)) [] |]
|
||||||
|
computed = ComputedEntry (ebHaskellName e) st link
|
||||||
|
|
||||||
|
-- | Create a 'ComputedEntry' for production mode, hashing and embedding the content into the executable.
|
||||||
|
prodEmbed :: Entry -> IO ComputedEntry
|
||||||
|
prodEmbed e = do
|
||||||
|
ct <- ebProductionContent e
|
||||||
|
let hash = base64md5 ct
|
||||||
|
link = [| EmbeddedResourceR (T.splitOn (T.pack "/") $ T.pack $(mkStr $ ebLocation e))
|
||||||
|
[(T.pack "etag", T.pack $(mkStr hash))] |]
|
||||||
|
st = Static.EmbeddableEntry {
|
||||||
|
Static.eLocation = "res/" `T.append` T.pack (ebLocation e)
|
||||||
|
, Static.eMimeType = ebMimeType e
|
||||||
|
, Static.eContent = Left (T.pack hash, ct)
|
||||||
|
}
|
||||||
|
return $ ComputedEntry (ebHaskellName e) st link
|
||||||
|
|
||||||
|
toApp :: (Request -> IO Response) -> Application
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
toApp f req g = f req >>= g
|
||||||
|
#else
|
||||||
|
toApp = id
|
||||||
|
#endif
|
||||||
|
|
||||||
|
tryExtraDevelFiles :: [[T.Text] -> IO (Maybe (MimeType, BL.ByteString))] -> Application
|
||||||
|
tryExtraDevelFiles = toApp . tryExtraDevelFiles'
|
||||||
|
|
||||||
|
tryExtraDevelFiles' :: [[T.Text] -> IO (Maybe (MimeType, BL.ByteString))] -> Request -> IO Response
|
||||||
|
tryExtraDevelFiles' [] _ = return $ responseLBS status404 [] ""
|
||||||
|
tryExtraDevelFiles' (f:fs) r = do
|
||||||
|
mct <- liftIO $ f $ drop 1 $ pathInfo r -- drop the initial "res"
|
||||||
|
case mct of
|
||||||
|
Nothing -> tryExtraDevelFiles' fs r
|
||||||
|
Just (mime, ct) -> do
|
||||||
|
let hash = T.encodeUtf8 $ T.pack $ base64md5 ct
|
||||||
|
let headers = [ ("Content-Type", mime)
|
||||||
|
, ("ETag", hash)
|
||||||
|
]
|
||||||
|
case lookup "If-None-Match" (requestHeaders r) of
|
||||||
|
Just h | hash == h -> return $ responseLBS status304 headers ""
|
||||||
|
_ -> return $ responseLBS status200 headers ct
|
||||||
|
|
||||||
|
-- | Helper to create the development application at runtime
|
||||||
|
develApp :: StaticSettings -> [[T.Text] -> IO (Maybe (MimeType, BL.ByteString))] -> Application
|
||||||
|
#if MIN_VERSION_wai(3, 0, 0)
|
||||||
|
develApp settings extra req sendResponse = do
|
||||||
|
staticApp settings {ssMaxAge = NoMaxAge} req $ \resp ->
|
||||||
|
if statusCode (responseStatus resp) == 404
|
||||||
|
then tryExtraDevelFiles extra req sendResponse
|
||||||
|
else sendResponse resp
|
||||||
|
#else
|
||||||
|
develApp settings extra req = do
|
||||||
|
resp <- staticApp settings {ssMaxAge = NoMaxAge} req
|
||||||
|
if statusCode (responseStatus resp) == 404
|
||||||
|
then tryExtraDevelFiles extra req
|
||||||
|
else return resp
|
||||||
|
#endif
|
||||||
|
|
||||||
|
-- | The type of 'addStaticContent'
|
||||||
|
type AddStaticContent site = T.Text -> T.Text -> BL.ByteString
|
||||||
|
-> HandlerT site IO (Maybe (Either T.Text (Route site, [(T.Text, T.Text)])))
|
||||||
|
|
||||||
|
-- | Helper for embedStaticContent and embedLicensedStaticContent.
|
||||||
|
staticContentHelper :: Yesod site
|
||||||
|
=> (site -> EmbeddedStatic)
|
||||||
|
-> (Route EmbeddedStatic -> Route site)
|
||||||
|
-> (BL.ByteString -> Either a BL.ByteString)
|
||||||
|
-> AddStaticContent site
|
||||||
|
staticContentHelper getStatic staticR minify ext _ ct = do
|
||||||
|
wIORef <- widgetFiles . getStatic <$> getYesod
|
||||||
|
let hash = T.pack $ base64md5 ct
|
||||||
|
hash' = Just $ T.encodeUtf8 hash
|
||||||
|
filename = T.concat [hash, ".", ext]
|
||||||
|
content = case ext of
|
||||||
|
"js" -> either (const ct) id $ minify ct
|
||||||
|
_ -> ct
|
||||||
|
file = File
|
||||||
|
{ fileGetSize = fromIntegral $ BL.length content
|
||||||
|
, fileToResponse = \s h -> responseLBS s h content
|
||||||
|
, fileName = unsafeToPiece filename
|
||||||
|
, fileGetHash = return hash'
|
||||||
|
, fileGetModified = Nothing
|
||||||
|
}
|
||||||
|
liftIO $ atomicModifyIORef' wIORef $ \m ->
|
||||||
|
(M.insertWith (\old _ -> old) filename file m, ())
|
||||||
|
|
||||||
|
return $ Just $ Right (staticR $ EmbeddedWidgetR filename, [])
|
||||||
|
|
||||||
|
-- | Create a wai-app-static settings based on the IORef inside the EmbeddedStaic site.
|
||||||
|
widgetSettings :: EmbeddedStatic -> StaticSettings
|
||||||
|
widgetSettings es = (defaultWebAppSettings "") { ssLookupFile = lookupFile }
|
||||||
|
where
|
||||||
|
lookupFile [_,p] = do -- The first part of the path is "widget"
|
||||||
|
m <- readIORef $ widgetFiles es
|
||||||
|
return $ maybe LRNotFound LRFile $ M.lookup (fromPiece p) m
|
||||||
|
lookupFile _ = return LRNotFound
|
||||||
67
yesod-static/Yesod/EmbeddedStatic/Types.hs
Normal file
67
yesod-static/Yesod/EmbeddedStatic/Types.hs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
|
||||||
|
module Yesod.EmbeddedStatic.Types(
|
||||||
|
Location
|
||||||
|
, Generator
|
||||||
|
-- ** Entry
|
||||||
|
, Entry
|
||||||
|
, ebHaskellName
|
||||||
|
, ebLocation
|
||||||
|
, ebMimeType
|
||||||
|
, ebProductionContent
|
||||||
|
, ebDevelReload
|
||||||
|
, ebDevelExtraFiles
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Default
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Network.Mime (MimeType)
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
|
-- | A location is a relative path within the static subsite at which resource(s) are made available.
|
||||||
|
-- The location can include slashes to simulate directories but must not start or end with a slash.
|
||||||
|
type Location = String
|
||||||
|
|
||||||
|
-- | A single resource embedded into the executable at compile time.
|
||||||
|
--
|
||||||
|
-- This data type is a settings type. For more information, see
|
||||||
|
-- <http://www.yesodweb.com/book/settings-types>.
|
||||||
|
data Entry = Entry {
|
||||||
|
ebHaskellName :: Maybe Name
|
||||||
|
-- ^ An optional haskell name. If the name is present, a variable
|
||||||
|
-- of type @Route 'Yesod.EmbeddedStatic.EmbeddedStatic'@ with the
|
||||||
|
-- given name will be created which points to this resource.
|
||||||
|
, ebLocation :: Location -- ^ The location to serve the resource from.
|
||||||
|
, ebMimeType :: MimeType -- ^ The mime type of the resource.
|
||||||
|
, ebProductionContent :: IO BL.ByteString
|
||||||
|
-- ^ If the development argument to 'Yesod.EmbeddedStatic.mkEmbeddedStatic' is False,
|
||||||
|
-- then at compile time this action will be executed to load the content.
|
||||||
|
-- During development, this action will not be executed.
|
||||||
|
, ebDevelReload :: ExpQ
|
||||||
|
-- ^ This must be a template haskell expression of type @IO 'BL.ByteString'@.
|
||||||
|
-- If the development argument to 'Yesod.EmbeddedStatic.mkEmbeddedStatic' is True,
|
||||||
|
-- this action is executed on every request to compute the content. Most of the
|
||||||
|
-- time, 'ebProductionContent' and 'ebDevelReload' should be the same action but
|
||||||
|
-- occasionally you might want additional processing inside the 'ebProductionContent'
|
||||||
|
-- function like javascript/css minification to only happen when building for production.
|
||||||
|
, ebDevelExtraFiles :: Maybe ExpQ
|
||||||
|
-- ^ Occasionally, during development an entry needs extra files/resources available
|
||||||
|
-- that are not present during production (for example, image files that are embedded
|
||||||
|
-- into the CSS at production but left unembedded during development). If present,
|
||||||
|
-- @ebDevelExtraFiles@ must be a template haskell expression of type
|
||||||
|
-- @['T.Text'] -> IO (Maybe ('MimeType', 'BL.ByteString'))@. That is, a function
|
||||||
|
-- taking as input the list of path pieces and optionally returning a mime type
|
||||||
|
-- and content.
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | When using 'def', you must fill in at least 'ebLocation'.
|
||||||
|
instance Default Entry where
|
||||||
|
def = Entry { ebHaskellName = Nothing
|
||||||
|
, ebLocation = "xxxx"
|
||||||
|
, ebMimeType = "application/octet-stream"
|
||||||
|
, ebProductionContent = return BL.empty
|
||||||
|
, ebDevelReload = [| return BL.empty |]
|
||||||
|
, ebDevelExtraFiles = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | An embedded generator is executed at compile time to produce the entries to embed.
|
||||||
|
type Generator = Q [Entry]
|
||||||
@ -35,7 +35,6 @@ module Yesod.Static
|
|||||||
-- * Smart constructor
|
-- * Smart constructor
|
||||||
, static
|
, static
|
||||||
, staticDevel
|
, staticDevel
|
||||||
, embed
|
|
||||||
-- * Combining CSS/JS
|
-- * Combining CSS/JS
|
||||||
-- $combining
|
-- $combining
|
||||||
, combineStylesheets'
|
, combineStylesheets'
|
||||||
@ -54,6 +53,8 @@ module Yesod.Static
|
|||||||
, publicFiles
|
, publicFiles
|
||||||
-- * Hashing
|
-- * Hashing
|
||||||
, base64md5
|
, base64md5
|
||||||
|
-- * Embed
|
||||||
|
, embed
|
||||||
#ifdef TEST_EXPORT
|
#ifdef TEST_EXPORT
|
||||||
, getFileListPieces
|
, getFileListPieces
|
||||||
#endif
|
#endif
|
||||||
@ -65,6 +66,7 @@ import System.Directory
|
|||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.FileEmbed (embedDir)
|
import Data.FileEmbed (embedDir)
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Resource (runResourceT)
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
|
|
||||||
@ -72,14 +74,14 @@ import Data.List (intercalate)
|
|||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
import Language.Haskell.TH.Syntax as TH
|
import Language.Haskell.TH.Syntax as TH
|
||||||
|
|
||||||
import Crypto.Conduit (hashFile, sinkHash)
|
import Crypto.Hash.Conduit (hashFile, sinkHash)
|
||||||
import Crypto.Hash.MD5 (MD5)
|
import Crypto.Hash (MD5, Digest)
|
||||||
import Control.Monad.Trans.State
|
import Control.Monad.Trans.State
|
||||||
|
|
||||||
|
import qualified Data.Byteable as Byteable
|
||||||
import qualified Data.ByteString.Base64
|
import qualified Data.ByteString.Base64
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import qualified Data.Serialize
|
|
||||||
import Data.Text (Text, pack)
|
import Data.Text (Text, pack)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
@ -100,7 +102,7 @@ import Filesystem (createTree)
|
|||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import qualified Data.Text.Lazy.Encoding as TLE
|
import qualified Data.Text.Lazy.Encoding as TLE
|
||||||
import Data.Default
|
import Data.Default
|
||||||
import Text.Lucius (luciusRTMinified)
|
--import Text.Lucius (luciusRTMinified)
|
||||||
|
|
||||||
import Network.Wai.Application.Static
|
import Network.Wai.Application.Static
|
||||||
( StaticSettings (..)
|
( StaticSettings (..)
|
||||||
@ -134,8 +136,11 @@ staticDevel dir = do
|
|||||||
hashLookup <- cachedETagLookupDevel dir
|
hashLookup <- cachedETagLookupDevel dir
|
||||||
return $ Static $ webAppSettingsWithLookup (F.decodeString dir) hashLookup
|
return $ Static $ webAppSettingsWithLookup (F.decodeString dir) hashLookup
|
||||||
|
|
||||||
-- | Produce a 'Static' based on embedding all of the static
|
-- | Produce a 'Static' based on embedding all of the static files' contents in the
|
||||||
-- files' contents in the executable at compile time.
|
-- executable at compile time.
|
||||||
|
--
|
||||||
|
-- You should use "Yesod.EmbeddedStatic" instead, it is much more powerful.
|
||||||
|
--
|
||||||
-- Nota Bene: if you replace the scaffolded 'static' call in Settings/StaticFiles.hs
|
-- Nota Bene: if you replace the scaffolded 'static' call in Settings/StaticFiles.hs
|
||||||
-- you will need to change the scaffolded addStaticContent. Otherwise, some of your
|
-- you will need to change the scaffolded addStaticContent. Otherwise, some of your
|
||||||
-- assets will be 404'ed. This is because by default yesod will generate compile those
|
-- assets will be 404'ed. This is because by default yesod will generate compile those
|
||||||
@ -222,7 +227,7 @@ getFileListPieces = flip evalStateT M.empty . flip go id
|
|||||||
-- definitions would be created:
|
-- definitions would be created:
|
||||||
--
|
--
|
||||||
-- > style_css = StaticRoute ["style.css"] []
|
-- > style_css = StaticRoute ["style.css"] []
|
||||||
-- > js_script_js = StaticRoute ["js/script.js"] []
|
-- > js_script_js = StaticRoute ["js", "script.js"] []
|
||||||
--
|
--
|
||||||
-- Note that dots (@.@), dashes (@-@) and slashes (@\/@) are
|
-- Note that dots (@.@), dashes (@-@) and slashes (@\/@) are
|
||||||
-- replaced by underscores (@\_@) to create valid Haskell
|
-- replaced by underscores (@\_@) to create valid Haskell
|
||||||
@ -355,7 +360,7 @@ mkStaticFilesList fp fs routeConName makeHash = do
|
|||||||
|
|
||||||
base64md5File :: Prelude.FilePath -> IO String
|
base64md5File :: Prelude.FilePath -> IO String
|
||||||
base64md5File = fmap (base64 . encode) . hashFile
|
base64md5File = fmap (base64 . encode) . hashFile
|
||||||
where encode d = Data.Serialize.encode (d :: MD5)
|
where encode d = Byteable.toBytes (d :: Digest MD5)
|
||||||
|
|
||||||
base64md5 :: L.ByteString -> String
|
base64md5 :: L.ByteString -> String
|
||||||
base64md5 lbs =
|
base64md5 lbs =
|
||||||
@ -363,7 +368,7 @@ base64md5 lbs =
|
|||||||
$ runIdentity
|
$ runIdentity
|
||||||
$ sourceList (L.toChunks lbs) $$ sinkHash
|
$ sourceList (L.toChunks lbs) $$ sinkHash
|
||||||
where
|
where
|
||||||
encode d = Data.Serialize.encode (d :: MD5)
|
encode d = Byteable.toBytes (d :: Digest MD5)
|
||||||
|
|
||||||
base64 :: S.ByteString -> String
|
base64 :: S.ByteString -> String
|
||||||
base64 = map tr
|
base64 = map tr
|
||||||
@ -442,7 +447,7 @@ data CombineSettings = CombineSettings
|
|||||||
, csCssPostProcess :: [FilePath] -> L.ByteString -> IO L.ByteString
|
, csCssPostProcess :: [FilePath] -> L.ByteString -> IO L.ByteString
|
||||||
-- ^ Post processing to be performed on CSS files.
|
-- ^ Post processing to be performed on CSS files.
|
||||||
--
|
--
|
||||||
-- Default: Use Lucius to minify.
|
-- Default: Pass-through.
|
||||||
--
|
--
|
||||||
-- Since 1.2.0
|
-- Since 1.2.0
|
||||||
, csJsPostProcess :: [FilePath] -> L.ByteString -> IO L.ByteString
|
, csJsPostProcess :: [FilePath] -> L.ByteString -> IO L.ByteString
|
||||||
@ -474,10 +479,13 @@ data CombineSettings = CombineSettings
|
|||||||
instance Default CombineSettings where
|
instance Default CombineSettings where
|
||||||
def = CombineSettings
|
def = CombineSettings
|
||||||
{ csStaticDir = "static"
|
{ csStaticDir = "static"
|
||||||
|
{- Disabled due to: https://github.com/yesodweb/yesod/issues/623
|
||||||
, csCssPostProcess = \fps ->
|
, csCssPostProcess = \fps ->
|
||||||
either (error . (errorIntro fps)) (return . TLE.encodeUtf8)
|
either (error . (errorIntro fps)) (return . TLE.encodeUtf8)
|
||||||
. flip luciusRTMinified []
|
. flip luciusRTMinified []
|
||||||
. TLE.decodeUtf8
|
. TLE.decodeUtf8
|
||||||
|
-}
|
||||||
|
, csCssPostProcess = const return
|
||||||
, csJsPostProcess = const return
|
, csJsPostProcess = const return
|
||||||
-- FIXME The following borders on a hack. With combining of files,
|
-- FIXME The following borders on a hack. With combining of files,
|
||||||
-- the final location of the CSS is no longer fixed, so relative
|
-- the final location of the CSS is no longer fixed, so relative
|
||||||
|
|||||||
@ -1,23 +1,42 @@
|
|||||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-}
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
-- | This embeds just a single file; it embeds the source code file
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
-- \"sample-embed.hs\" from the current directory so when you compile,
|
||||||
import Yesod.Static
|
-- the sample-embed.hs file must be in the current directory.
|
||||||
import Yesod.Dispatch
|
--
|
||||||
|
-- Try toggling the development argument to 'mkEmbeddedStatic'. When the
|
||||||
|
-- development argument is true the file \"sample-embed.hs\" is reloaded
|
||||||
|
-- from disk on every request (try changing it after you start the server).
|
||||||
|
-- When development is false, the contents are embedded and the sample-embed.hs
|
||||||
|
-- file does not even need to be present during runtime.
|
||||||
|
module Main where
|
||||||
|
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Network.Wai.Handler.Warp (run)
|
import Yesod.EmbeddedStatic
|
||||||
|
|
||||||
staticFiles "."
|
mkEmbeddedStatic False "eStatic" [embedFile "sample-embed.hs"]
|
||||||
|
|
||||||
data Sample = Sample
|
-- The above will generate variables
|
||||||
getStatic _ = $(embed "tests")
|
-- eStatic :: EmbeddedStatic
|
||||||
mkYesod "Sample" [parseRoutes|
|
-- sample_embed_hs :: Route EmbeddedStatic
|
||||||
/ RootR GET
|
|
||||||
/static StaticR Static getStatic
|
data MyApp = MyApp { getStatic :: EmbeddedStatic }
|
||||||
|
|
||||||
|
mkYesod "MyApp" [parseRoutes|
|
||||||
|
/ HomeR GET
|
||||||
|
/static StaticR EmbeddedStatic getStatic
|
||||||
|]
|
|]
|
||||||
instance Yesod Sample where approot _ = ""
|
|
||||||
|
|
||||||
getRootR = do
|
instance Yesod MyApp where
|
||||||
redirectText RedirectPermanent "static"
|
addStaticContent = embedStaticContent getStatic StaticR Right
|
||||||
return ()
|
|
||||||
|
|
||||||
main = toWaiApp Sample >>= run 3000
|
getHomeR :: Handler Html
|
||||||
|
getHomeR = defaultLayout $ do
|
||||||
|
toWidget [julius|console.log("Hello World");|]
|
||||||
|
[whamlet|
|
||||||
|
<h1>Hello
|
||||||
|
<p>Check the
|
||||||
|
<a href=@{StaticR sample_embed_hs}>embedded file
|
||||||
|
|]
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = warp 3000 $ MyApp eStatic
|
||||||
|
|||||||
95
yesod-static/test/EmbedDevelTest.hs
Normal file
95
yesod-static/test/EmbedDevelTest.hs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies, OverloadedStrings #-}
|
||||||
|
module EmbedDevelTest where
|
||||||
|
|
||||||
|
-- Tests the development mode of the embedded static subsite by
|
||||||
|
-- using a custom generator testGen.
|
||||||
|
|
||||||
|
import Data.Maybe (isNothing)
|
||||||
|
import EmbedTestGenerator
|
||||||
|
import EmbedProductionTest (findEtag)
|
||||||
|
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||||
|
import Test.HUnit (assertBool)
|
||||||
|
import Test.Hspec (Spec)
|
||||||
|
import Yesod.Core
|
||||||
|
import Yesod.EmbeddedStatic
|
||||||
|
import Yesod.Test
|
||||||
|
|
||||||
|
mkEmbeddedStatic True "eDev" [testGen]
|
||||||
|
|
||||||
|
data MyApp = MyApp { getStatic :: EmbeddedStatic }
|
||||||
|
|
||||||
|
mkYesod "MyApp" [parseRoutes|
|
||||||
|
/static StaticR EmbeddedStatic getStatic
|
||||||
|
|]
|
||||||
|
|
||||||
|
instance Yesod MyApp
|
||||||
|
|
||||||
|
noCacheControl :: YesodExample site ()
|
||||||
|
noCacheControl = withResponse $ \r -> do
|
||||||
|
liftIO $ assertBool "Cache-Control exists" $
|
||||||
|
isNothing $ lookup "Cache-Control" $ simpleHeaders r
|
||||||
|
liftIO $ assertBool "Expires exists" $
|
||||||
|
isNothing $ lookup "Expires" $ simpleHeaders r
|
||||||
|
|
||||||
|
embedDevSpecs :: Spec
|
||||||
|
embedDevSpecs = yesodSpec (MyApp eDev) $ do
|
||||||
|
ydescribe "Embedded Development Entries" $ do
|
||||||
|
yit "e1 loads" $ do
|
||||||
|
get $ StaticR e1
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "text/plain"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "e1 devel"
|
||||||
|
|
||||||
|
tag <- findEtag
|
||||||
|
request $ do
|
||||||
|
setMethod "GET"
|
||||||
|
setUrl $ StaticR e1
|
||||||
|
addRequestHeader ("If-None-Match", tag)
|
||||||
|
statusIs 304
|
||||||
|
|
||||||
|
yit "e2 with simulated directory" $ do
|
||||||
|
get $ StaticR e2
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "abcdef"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "e2 devel"
|
||||||
|
|
||||||
|
yit "e3 without haskell name" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["xxxx", "e3"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "yyy"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "e3 devel"
|
||||||
|
|
||||||
|
yit "e4 loads" $ do
|
||||||
|
get $ StaticR e4
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "text/plain"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "e4 devel"
|
||||||
|
|
||||||
|
yit "e4 extra development dev1" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["dev1"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "mime"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "dev1 content"
|
||||||
|
|
||||||
|
tag <- findEtag
|
||||||
|
request $ do
|
||||||
|
setMethod "GET"
|
||||||
|
setUrl $ StaticR $ embeddedResourceR ["dev1"] []
|
||||||
|
addRequestHeader ("If-None-Match", tag)
|
||||||
|
statusIs 304
|
||||||
|
|
||||||
|
yit "e4 extra development with path" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["dir", "dev2"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "mime2"
|
||||||
|
noCacheControl
|
||||||
|
bodyEquals "dev2 content"
|
||||||
|
|
||||||
|
yit "extra development file 404" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["xxxxxxxxxx"] []
|
||||||
|
statusIs 404
|
||||||
118
yesod-static/test/EmbedProductionTest.hs
Normal file
118
yesod-static/test/EmbedProductionTest.hs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies, OverloadedStrings #-}
|
||||||
|
module EmbedProductionTest where
|
||||||
|
|
||||||
|
-- Tests the production mode of the embedded static subsite by
|
||||||
|
-- using a custom generator testGen. Also tests that the widget
|
||||||
|
-- content is embedded properly.
|
||||||
|
|
||||||
|
import Data.Maybe (isJust)
|
||||||
|
import EmbedTestGenerator
|
||||||
|
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||||
|
import Test.HUnit (assertFailure, assertBool)
|
||||||
|
import Test.Hspec (Spec)
|
||||||
|
import Yesod.Core
|
||||||
|
import Yesod.EmbeddedStatic
|
||||||
|
import Yesod.Test
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.Lazy.Encoding as TL
|
||||||
|
|
||||||
|
mkEmbeddedStatic False "eProduction" [testGen]
|
||||||
|
|
||||||
|
data MyApp = MyApp { getStatic :: EmbeddedStatic }
|
||||||
|
|
||||||
|
mkYesod "MyApp" [parseRoutes|
|
||||||
|
/ HomeR GET
|
||||||
|
/static StaticR EmbeddedStatic getStatic
|
||||||
|
|]
|
||||||
|
|
||||||
|
getHomeR :: Handler Html
|
||||||
|
getHomeR = defaultLayout $ do
|
||||||
|
toWidget [julius|console.log("Hello World");|]
|
||||||
|
[whamlet|<h1>Hello|]
|
||||||
|
|
||||||
|
instance Yesod MyApp where
|
||||||
|
addStaticContent = embedStaticContent getStatic StaticR Right
|
||||||
|
|
||||||
|
findEtag :: YesodExample site B.ByteString
|
||||||
|
findEtag = withResponse $ \r ->
|
||||||
|
case lookup "ETag" (simpleHeaders r) of
|
||||||
|
Nothing -> liftIO (assertFailure "No etag found") >> error ""
|
||||||
|
Just e -> return e
|
||||||
|
|
||||||
|
hasCacheControl :: YesodExample site ()
|
||||||
|
hasCacheControl = withResponse $ \r -> do
|
||||||
|
liftIO $ assertBool "Cache-Control missing" $
|
||||||
|
isJust $ lookup "Cache-Control" $ simpleHeaders r
|
||||||
|
liftIO $ assertBool "Expires missing" $
|
||||||
|
isJust $ lookup "Expires" $ simpleHeaders r
|
||||||
|
|
||||||
|
embedProductionSpecs :: Spec
|
||||||
|
embedProductionSpecs = yesodSpec (MyApp eProduction) $ do
|
||||||
|
ydescribe "Embedded Production Entries" $ do
|
||||||
|
yit "e1 loads" $ do
|
||||||
|
get $ StaticR e1
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "text/plain"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e1 production"
|
||||||
|
|
||||||
|
tag <- findEtag
|
||||||
|
request $ do
|
||||||
|
setMethod "GET"
|
||||||
|
setUrl $ StaticR e1
|
||||||
|
addRequestHeader ("If-None-Match", tag)
|
||||||
|
statusIs 304
|
||||||
|
|
||||||
|
yit "e1 with custom built path" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["e1"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "text/plain"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e1 production"
|
||||||
|
|
||||||
|
yit "e2 with simulated directory" $ do
|
||||||
|
get $ StaticR e2
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "abcdef"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e2 production"
|
||||||
|
|
||||||
|
yit "e2 with custom built directory path" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["dir", "e2"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "abcdef"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e2 production"
|
||||||
|
|
||||||
|
yit "e3 without haskell name" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["xxxx", "e3"] []
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "yyy"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e3 production"
|
||||||
|
|
||||||
|
yit "e4 is embedded" $ do
|
||||||
|
get $ StaticR e4
|
||||||
|
statusIs 200
|
||||||
|
assertHeader "Content-Type" "text/plain"
|
||||||
|
hasCacheControl
|
||||||
|
bodyEquals "e4 production"
|
||||||
|
|
||||||
|
yit "e4 extra development files are not embedded" $ do
|
||||||
|
get $ StaticR $ embeddedResourceR ["dev1"] []
|
||||||
|
statusIs 404
|
||||||
|
|
||||||
|
ydescribe "Embedded Widget Content" $
|
||||||
|
yit "Embedded Javascript" $ do
|
||||||
|
get HomeR
|
||||||
|
statusIs 200
|
||||||
|
[script] <- htmlQuery "script"
|
||||||
|
let src = BL.takeWhile (/= 34) $ BL.drop 1 $ BL.dropWhile (/= 34) script -- 34 is "
|
||||||
|
|
||||||
|
get $ TL.toStrict $ TL.decodeUtf8 src
|
||||||
|
statusIs 200
|
||||||
|
hasCacheControl
|
||||||
|
assertHeader "Content-Type" "application/javascript"
|
||||||
|
bodyEquals "console.log(\"Hello World\");"
|
||||||
62
yesod-static/test/EmbedTestGenerator.hs
Normal file
62
yesod-static/test/EmbedTestGenerator.hs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
|
||||||
|
module EmbedTestGenerator (testGen) where
|
||||||
|
|
||||||
|
import Data.Default
|
||||||
|
import Network.Mime (MimeType)
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
import Yesod.EmbeddedStatic.Generators (pathToName)
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.Lazy.Encoding as TL
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
|
e1, e2, e3, e4 :: Entry
|
||||||
|
|
||||||
|
-- Basic entry
|
||||||
|
e1 = def
|
||||||
|
{ ebHaskellName = Just $ pathToName "e1"
|
||||||
|
, ebLocation = "e1"
|
||||||
|
, ebMimeType = "text/plain"
|
||||||
|
, ebProductionContent = return $ TL.encodeUtf8 "e1 production"
|
||||||
|
, ebDevelReload = [| return $ TL.encodeUtf8 $ TL.pack "e1 devel" |]
|
||||||
|
, ebDevelExtraFiles = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Test simulated directory in location
|
||||||
|
e2 = def
|
||||||
|
{ ebHaskellName = Just $ pathToName "e2"
|
||||||
|
, ebLocation = "dir/e2"
|
||||||
|
, ebMimeType = "abcdef"
|
||||||
|
, ebProductionContent = return $ TL.encodeUtf8 "e2 production"
|
||||||
|
, ebDevelReload = [| return $ TL.encodeUtf8 $ TL.pack "e2 devel" |]
|
||||||
|
, ebDevelExtraFiles = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Test empty haskell name
|
||||||
|
e3 = def
|
||||||
|
{ ebHaskellName = Nothing
|
||||||
|
, ebLocation = "xxxx/e3"
|
||||||
|
, ebMimeType = "yyy"
|
||||||
|
, ebProductionContent = return $ TL.encodeUtf8 "e3 production"
|
||||||
|
, ebDevelReload = [| return $ TL.encodeUtf8 $ TL.pack "e3 devel" |]
|
||||||
|
, ebDevelExtraFiles = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
devExtra :: [T.Text] -> IO (Maybe (MimeType, BL.ByteString))
|
||||||
|
devExtra ["dev1"] = return $ Just ("mime", "dev1 content")
|
||||||
|
devExtra ["dir", "dev2"] = return $ Just ("mime2", "dev2 content")
|
||||||
|
devExtra _ = return Nothing
|
||||||
|
|
||||||
|
-- Entry with devel extra files
|
||||||
|
e4 = def
|
||||||
|
{ ebHaskellName = Just $ pathToName "e4"
|
||||||
|
, ebLocation = "e4"
|
||||||
|
, ebMimeType = "text/plain"
|
||||||
|
, ebProductionContent = return $ TL.encodeUtf8 "e4 production"
|
||||||
|
, ebDevelReload = [| return $ TL.encodeUtf8 $ TL.pack "e4 devel" |]
|
||||||
|
, ebDevelExtraFiles = Just [| devExtra |]
|
||||||
|
}
|
||||||
|
|
||||||
|
testGen :: Generator
|
||||||
|
testGen = return [e1, e2, e3, e4]
|
||||||
92
yesod-static/test/FileGeneratorTests.hs
Normal file
92
yesod-static/test/FileGeneratorTests.hs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
|
||||||
|
module FileGeneratorTests (fileGenSpecs) where
|
||||||
|
|
||||||
|
import Control.Exception
|
||||||
|
import Control.Monad (forM_)
|
||||||
|
import GeneratorTestUtil
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.HUnit (assertFailure, assertEqual)
|
||||||
|
import Yesod.EmbeddedStatic.Generators
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
|
-- | Embeds the LICENSE file
|
||||||
|
license :: GenTestResult
|
||||||
|
license = $(embedFile "LICENSE" >>=
|
||||||
|
testOneEntry (Just "_LICENSE") "LICENSE" (BL.readFile "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
licenseAt :: GenTestResult
|
||||||
|
licenseAt = $(embedFileAt "abc.txt" "LICENSE" >>=
|
||||||
|
testOneEntry (Just "abc_txt") "abc.txt" (BL.readFile "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
embDir :: [GenTestResult]
|
||||||
|
embDir = $(embedDir "test/embed-dir" >>=
|
||||||
|
testEntries
|
||||||
|
[ (Just "abc_def_txt", "abc/def.txt", BL.readFile "test/embed-dir/abc/def.txt")
|
||||||
|
, (Just "lorem_txt", "lorem.txt", BL.readFile "test/embed-dir/lorem.txt")
|
||||||
|
, (Just "foo", "foo", BL.readFile "test/embed-dir/foo")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
embDirAt :: [GenTestResult]
|
||||||
|
embDirAt = $(embedDirAt "xxx" "test/embed-dir" >>=
|
||||||
|
testEntries
|
||||||
|
[ (Just "xxx_abc_def_txt", "xxx/abc/def.txt", BL.readFile "test/embed-dir/abc/def.txt")
|
||||||
|
, (Just "xxx_lorem_txt", "xxx/lorem.txt", BL.readFile "test/embed-dir/lorem.txt")
|
||||||
|
, (Just "xxx_foo", "xxx/foo", BL.readFile "test/embed-dir/foo")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
concatR :: GenTestResult
|
||||||
|
concatR = $(concatFiles "out.txt" [ "test/embed-dir/abc/def.txt", "test/embed-dir/foo"] >>=
|
||||||
|
testOneEntry (Just "out_txt") "out.txt" (return "Yesod Rocks\nBar\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
-- The transform function should only run at compile for the production content
|
||||||
|
concatWithR :: GenTestResult
|
||||||
|
concatWithR = $(concatFilesWith "out2.txt"
|
||||||
|
(\x -> return $ x `BL.append` "Extra")
|
||||||
|
[ "test/embed-dir/abc/def.txt", "test/embed-dir/foo"] >>=
|
||||||
|
testOneEntry (Just "out2_txt") "out2.txt" (return "Yesod Rocks\nBar\nExtra")
|
||||||
|
)
|
||||||
|
|
||||||
|
fileGenSpecs :: Spec
|
||||||
|
fileGenSpecs = do
|
||||||
|
describe "Embed File" $ do
|
||||||
|
it "embeds a single file" $
|
||||||
|
assertGenResult (BL.readFile "LICENSE") license
|
||||||
|
it "embeds a single file at a location" $
|
||||||
|
assertGenResult (BL.readFile "LICENSE") licenseAt
|
||||||
|
|
||||||
|
describe "Embed Directory" $ do
|
||||||
|
it "embeds a directory" $
|
||||||
|
forM_ [embDir, embDirAt] $ \d -> case d of
|
||||||
|
[GenError e] -> assertFailure e
|
||||||
|
[def, foo, lorem] -> do
|
||||||
|
assertGenResult (BL.readFile "test/embed-dir/abc/def.txt") def
|
||||||
|
assertGenResult (BL.readFile "test/embed-dir/foo") foo
|
||||||
|
assertGenResult (BL.readFile "test/embed-dir/lorem.txt") lorem
|
||||||
|
_ -> assertFailure "Bad directory list"
|
||||||
|
|
||||||
|
describe "Concat Files" $ do
|
||||||
|
it "simple concat" $
|
||||||
|
assertGenResult (return "Yesod Rocks\nBar\n") concatR
|
||||||
|
it "concat with processing function" $
|
||||||
|
assertGenResult (return "Yesod Rocks\nBar\n") concatWithR -- no Extra since this is development
|
||||||
|
|
||||||
|
describe "Compress" $ do
|
||||||
|
it "compress tool function" $ do
|
||||||
|
out <- compressTool "runhaskell" [] "main = putStrLn \"Hello World\""
|
||||||
|
assertEqual "" "Hello World\n" out
|
||||||
|
|
||||||
|
it "tryCompressTools" $ do
|
||||||
|
out <- flip tryCompressTools "abcdef"
|
||||||
|
[ const $ throwIO $ ErrorCall "An expected error"
|
||||||
|
, const $ return "foo"
|
||||||
|
, const $ return "bar"
|
||||||
|
]
|
||||||
|
assertEqual "" "foo" out
|
||||||
|
out2 <- flip tryCompressTools "abcdef"
|
||||||
|
[ const $ throwIO $ ErrorCall "An expected error"]
|
||||||
|
assertEqual "" "abcdef" out2
|
||||||
59
yesod-static/test/GeneratorTestUtil.hs
Normal file
59
yesod-static/test/GeneratorTestUtil.hs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
|
||||||
|
module GeneratorTestUtil where
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
import Control.Monad (when)
|
||||||
|
import Data.List (sortBy)
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Test.HUnit
|
||||||
|
import Yesod.EmbeddedStatic.Types
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
|
-- We test the generators by executing them at compile time
|
||||||
|
-- and sticking the result into the GenTestResult. We then
|
||||||
|
-- test the GenTestResult at runtime. But to test the ebDevelReload
|
||||||
|
-- we must run the action at runtime so that is also embedded.
|
||||||
|
-- Because of template haskell stage restrictions, this code
|
||||||
|
-- needs to be in a separate module.
|
||||||
|
|
||||||
|
data GenTestResult = GenError String
|
||||||
|
| GenSuccessWithDevel (IO BL.ByteString)
|
||||||
|
|
||||||
|
-- | Creates a GenTestResult at compile time by testing the entry.
|
||||||
|
testEntry :: Maybe String -> Location -> IO BL.ByteString -> Entry -> ExpQ
|
||||||
|
testEntry name _ _ e | ebHaskellName e /= (mkName <$> name) =
|
||||||
|
[| GenError ("haskell name " ++ $(litE $ stringL $ show $ ebHaskellName e)
|
||||||
|
++ " /= "
|
||||||
|
++ $(litE $ stringL $ show name)) |]
|
||||||
|
testEntry _ loc _ e | ebLocation e /= loc =
|
||||||
|
[| GenError ("location " ++ $(litE $ stringL $ show $ ebLocation e)) |]
|
||||||
|
testEntry _ _ act e = do
|
||||||
|
expected <- runIO act
|
||||||
|
actual <- runIO $ ebProductionContent e
|
||||||
|
if expected == actual
|
||||||
|
then [| GenSuccessWithDevel $(ebDevelReload e) |]
|
||||||
|
else [| GenError "production content" |]
|
||||||
|
|
||||||
|
testOneEntry :: Maybe String -> Location -> IO BL.ByteString -> [Entry] -> ExpQ
|
||||||
|
testOneEntry name loc ct [e] = testEntry name loc ct e
|
||||||
|
testOneEntry _ _ _ _ = [| GenError "not exactly one entry" |]
|
||||||
|
|
||||||
|
-- | Tests a list of entries
|
||||||
|
testEntries :: [(Maybe String, Location, IO BL.ByteString)] -> [Entry] -> ExpQ
|
||||||
|
testEntries a b | length a /= length b = [| [GenError "lengths differ"] |]
|
||||||
|
testEntries a b = listE $ zipWith f a' b'
|
||||||
|
where
|
||||||
|
a' = sortBy (\(_,l1,_) (_,l2,_) -> compare l1 l2) a
|
||||||
|
b' = sortBy (\e1 e2 -> ebLocation e1 `compare` ebLocation e2) b
|
||||||
|
f (name, loc, ct) e = testEntry name loc ct e
|
||||||
|
|
||||||
|
-- | Use this at runtime to assert the 'GenTestResult' is OK
|
||||||
|
assertGenResult :: (IO BL.ByteString) -- ^ expected development content
|
||||||
|
-> GenTestResult -- ^ test result created at compile time
|
||||||
|
-> Assertion
|
||||||
|
assertGenResult _ (GenError e) = assertFailure ("invalid " ++ e)
|
||||||
|
assertGenResult mexpected (GenSuccessWithDevel mactual) = do
|
||||||
|
expected <- mexpected
|
||||||
|
actual <- mactual
|
||||||
|
when (expected /= actual) $
|
||||||
|
assertFailure "invalid devel content"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user