diff --git a/.gitignore b/.gitignore index 2534db8d..0b1195cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.o +*.o_p *.hi dist *.swp client_session_key.aes +cabal-dev/ diff --git a/yesod-static/tests/unicode/LICENSE b/LICENSE similarity index 100% rename from yesod-static/tests/unicode/LICENSE rename to LICENSE diff --git a/README.md b/README.md index ec612e31..5d16687f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -A next generation web framework using the Haskell programming language, -featuring: +An advanced web framework using the Haskell programming language. Featuring: * safety & security guaranteed at compile time * performance @@ -12,25 +11,108 @@ featuring: ## Installation: http://www.yesodweb.com/page/five-minutes + cabal update && cabal install yesod + ## Create a new project after installing yesod init +## Using cabal-dev + +cabal-dev creates a sandboxed environment for an individual cabal package. +Your application is a cabal package and you should use cabal-dev with your Yesod application. +Instead of using the `cabal` command, use the `cabal-dev` command. + +Use `yesod-devel --dev` when developing your application. + ## Installing the latest development version from github -Yesod is built upon many smaller packages, all of which can be installed -with: +Yesod is broken up into 4 separate code repositories each built upon many smaller packages. + +Install conflicts are unfortunately common in Haskell development. +However, we can prevent most of them by using some extra tools. +This will require a little up-front reading and learning, but save you from a lot of misery in the long-run. +See the above explanation of cabal-dev, and below of virthualenv. + +Please note that cabal-dev will not work in a virthualenv shell - you can't use both at the same time. + +### virthualenv + +To just install Yesod from github, we only need cabal-dev. However, cabal-dev may be more hassle than it is worth when hacking on Yesod. + +We recommend using [virthualenv](http://hackage.haskell.org/package/virthualenv) when hacking on Yesod. +This is optional, but prevents your custom build of Yesod from interfering with your currently installed cabal packages. +virthualenv creates an isolated environment like cabal-dev. +cabal-dev isolates a single cabal package, but virthualenv isolates multiple packages together. + +virthualenv works at the shell level, so every shell must activate the virthualenv. + +### cabal-src + +Michael just released the cabal-src tool. Whenever you would use `cabal install` for a local package, use `cabal-src-install` instead. +Our installer script now uses cabal-src-install when it is available. + +### Building Yesod ~~~ { .bash } +# update your package database if you haven't recently cabal update +# install required libraries +cabal install Cabal cabal-install cabal-src virthualenv +# clone and install all repos +# see below about first using virthualenv before running ./scripts/install for repo in hamlet persistent wai yesod; do git clone http://github.com/yesodweb/$repo ( cd $repo git submodule update --init - ./script/install + ./scripts/install ) done ~~~ + +### Hacking on Yesod + +To prevent Yesod from conflicting with your other installs, you should use virthualenv, although it is optional. + +#### virthualenv + +~~~ { .bash } +cabal update +cabal install virthualenv +cd yesodweb +virthualenv --name=yesod +. .virthualenv/bin/activate +~~~ + +#### individual cabal packages + +~~~ { .bash } +# install and test all packages +./scripts/install + +# move to the individual package you are working on +cd shakespeare-text + +# build and test the individual package +cabal configure -ftest --enable-tests +cabal build +cabal test +~~~ + +#### cabal-dev + +cabal-dev works very well if you are working on a single package, but it can be very cumbersome to work on multiple packages at once. + +### Use your development version of Yesod in your application + +Note that we have told you to install Yesod into a sandboxed virthualenv environment. +This means it is not available through your user/global cabal database for your application. +Instead you should use `cabal-dev install` to retrieve these packages. +cd to your application directory, and the reference the source list. + +~~~ { .bash } +cabal-dev install /path/to/yesodweb/yesod/*(/) +~~~ diff --git a/input b/input new file mode 120000 index 00000000..83a4d036 --- /dev/null +++ b/input @@ -0,0 +1 @@ +yesod/input \ No newline at end of file diff --git a/package-list.sh b/package-list.sh new file mode 100644 index 00000000..2205ad1d --- /dev/null +++ b/package-list.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +pkgs=( ./yesod-core + ./yesod-json + ./yesod-static + ./yesod-persistent + ./yesod-newsfeed + ./yesod-form + ./yesod-auth + ./yesod-sitemap + ./yesod-default + ./yesod ) diff --git a/scaffold b/scaffold new file mode 120000 index 00000000..dde8823c --- /dev/null +++ b/scaffold @@ -0,0 +1 @@ +yesod/scaffold \ No newline at end of file diff --git a/scripts b/scripts index f56426fa..713588bc 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f56426fada59012329f23c928a2d7f9c3a515d75 +Subproject commit 713588bcf3526aad8a809215fb34c314334a5ffd diff --git a/sources.txt b/sources.txt new file mode 100644 index 00000000..60ba2fa0 --- /dev/null +++ b/sources.txt @@ -0,0 +1,10 @@ +yesod-core +yesod-json +yesod-static +yesod-persistent +yesod-newsfeed +yesod-form +yesod-auth +yesod-sitemap +yesod-default +yesod diff --git a/test/en.msg b/test/en.msg new file mode 120000 index 00000000..e4db7367 --- /dev/null +++ b/test/en.msg @@ -0,0 +1 @@ +../yesod-core/test/en.msg \ No newline at end of file diff --git a/test/fs b/test/fs new file mode 120000 index 00000000..034a0f64 --- /dev/null +++ b/test/fs @@ -0,0 +1 @@ +../yesod-static/test/fs \ No newline at end of file diff --git a/test/main.hs b/test/main.hs new file mode 100644 index 00000000..6eecda86 --- /dev/null +++ b/test/main.hs @@ -0,0 +1,9 @@ +import Test.Hspec +import qualified YesodCoreTest +import qualified YesodStaticTest + +main :: IO () +main = hspecX $ descriptions [ + concat YesodCoreTest.specs + , concat YesodStaticTest.specs + ] diff --git a/yesod-auth/Yesod/Auth.hs b/yesod-auth/Yesod/Auth.hs index ae5e5074..0034ae5d 100644 --- a/yesod-auth/Yesod/Auth.hs +++ b/yesod-auth/Yesod/Auth.hs @@ -34,7 +34,11 @@ import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode) import Data.Text (Text) import qualified Data.Text as T +#if MIN_VERSION_aeson(0, 4, 0) +import qualified Data.HashMap.Lazy as Map +#else import qualified Data.Map as Map +#endif import Language.Haskell.TH.Syntax hiding (lift) @@ -96,6 +100,11 @@ class (Yesod m, SinglePiece (AuthId m), RenderMessage m FormMessage) => YesodAut -> AuthMessage -> Text renderAuthMessage _ _ = defaultMessage + -- | After login and logout, redirect to the referring page, instead of + -- 'loginDest' and 'logoutDest'. Default is 'False'. + redirectToReferer :: m -> Bool + redirectToReferer _ = False + mkYesodSub "Auth" [ ClassP ''YesodAuth [VarT $ mkName "master"] ] @@ -134,7 +143,7 @@ getCheckR = do creds <- maybeAuthId defaultLayoutJson (do setTitle "Authentication Status" - addHtml $ html' creds) (json' creds) + addHtml $ html' creds) (jsonCreds creds) where html' creds = [QQ(shamlet)| @@ -144,16 +153,21 @@ $maybe _ <- creds $nothing

Not logged in. |] - json' creds = + jsonCreds creds = Object $ Map.fromList [ (T.pack "logged_in", Bool $ maybe False (const True) creds) ] +setUltDestReferer' :: YesodAuth master => GHandler sub master () +setUltDestReferer' = do + m <- getYesod + when (redirectToReferer m) setUltDestReferer + getLoginR :: YesodAuth m => GHandler Auth m RepHtml -getLoginR = setUltDestReferer >> loginHandler +getLoginR = setUltDestReferer' >> loginHandler getLogoutR :: YesodAuth m => GHandler Auth m () -getLogoutR = setUltDestReferer >> postLogoutR -- FIXME redirect to post +getLogoutR = setUltDestReferer' >> postLogoutR -- FIXME redirect to post postLogoutR :: YesodAuth m => GHandler Auth m () postLogoutR = do diff --git a/yesod-auth/Yesod/Auth/Email.hs b/yesod-auth/Yesod/Auth/Email.hs index 243af776..d6ae02a8 100644 --- a/yesod-auth/Yesod/Auth/Email.hs +++ b/yesod-auth/Yesod/Auth/Email.hs @@ -203,7 +203,7 @@ getPasswordR = do Just _ -> return () Nothing -> do setMessageI Msg.BadSetPass - redirect RedirectTemporary $ toMaster loginR + redirect RedirectTemporary $ toMaster LoginR defaultLayout $ do setTitleI Msg.SetPassTitle addWidget @@ -238,7 +238,7 @@ postPasswordR = do aid <- case maid of Nothing -> do setMessageI Msg.BadSetPass - redirect RedirectTemporary $ toMaster loginR + redirect RedirectTemporary $ toMaster LoginR Just aid -> return aid salted <- liftIO $ saltPass new setPassword aid salted diff --git a/yesod-auth/Yesod/Auth/Facebook.hs b/yesod-auth/Yesod/Auth/Facebook.hs index f8715e63..c4dbd1c0 100644 --- a/yesod-auth/Yesod/Auth/Facebook.hs +++ b/yesod-auth/Yesod/Auth/Facebook.hs @@ -3,7 +3,10 @@ {-# LANGUAGE OverloadedStrings #-} module Yesod.Auth.Facebook ( authFacebook + , facebookLogin , facebookUrl + , facebookLogout + , getFacebookAccessToken ) where #include "qq.h" @@ -17,20 +20,48 @@ import Data.Maybe (fromMaybe) import Yesod.Form import Yesod.Handler import Yesod.Widget -import Control.Monad.IO.Class (liftIO) +import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Class (lift) import Data.Text (Text) -import Control.Monad (mzero) +import Control.Monad (liftM, mzero, when) import Data.Monoid (mappend) import qualified Data.Aeson.Types import qualified Yesod.Auth.Message as Msg -facebookUrl :: AuthRoute -facebookUrl = PluginR "facebook" ["forward"] +-- | Route for login using this authentication plugin. +facebookLogin :: AuthRoute +facebookLogin = PluginR "facebook" ["forward"] +-- | This is just a synonym of 'facebookLogin'. Deprecated since +-- @yesod-auth 0.7.8@, please use 'facebookLogin' instead. +facebookUrl :: AuthRoute +facebookUrl = facebookLogin +{-# DEPRECATED facebookUrl "Please use facebookLogin instead." #-} + +-- | Route for logout using this authentication plugin. Per +-- Facebook's policies +-- (), the user needs to +-- logout from Facebook itself as well. +facebookLogout :: AuthRoute +facebookLogout = PluginR "facebook" ["logout"] + +-- | Get Facebook's access token from the session. Returns +-- @Nothing@ if it's not found (probably because the user is not +-- logged in via Facebook). Note that the returned access token +-- may have expired. +getFacebookAccessToken :: MonadIO mo => GGHandler sub master mo (Maybe Facebook.AccessToken) +getFacebookAccessToken = + liftM (fmap Facebook.AccessToken) (lookupSession facebookAccessTokenKey) + +-- | Key used to store Facebook's access token in the client +-- session. +facebookAccessTokenKey :: Text +facebookAccessTokenKey = "_FB" + +-- | Authentication plugin using Facebook. authFacebook :: YesodAuth m - => Text -- ^ Application ID - -> Text -- ^ Application secret + => Text -- ^ Application ID + -> Text -- ^ Application secret -> [Text] -- ^ Requested permissions -> AuthPlugin m authFacebook cid secret perms = @@ -49,10 +80,24 @@ authFacebook cid secret perms = code <- runInputGet $ ireq textField "code" at <- liftIO $ Facebook.getAccessToken fb code let Facebook.AccessToken at' = at + setSession facebookAccessTokenKey at' so <- liftIO $ Facebook.getGraphData at "me" let c = fromMaybe (error "Invalid response from Facebook") $ parseMaybe (parseCreds at') $ either error id so setCreds True c + dispatch "GET" ["logout"] = do + m <- getYesod + tm <- getRouteToMaster + mtoken <- getFacebookAccessToken + when (redirectToReferer m) setUltDestReferer + case mtoken of + Nothing -> do + -- Well... then just logout from our app. + redirect RedirectTemporary (tm LogoutR) + Just at -> do + render <- getUrlRender + let logout = Facebook.getLogoutUrl at (render $ tm LogoutR) + redirectText RedirectTemporary logout dispatch _ _ = notFound login tm = do render <- lift getUrlRender @@ -67,8 +112,8 @@ parseCreds :: Text -> Value -> Data.Aeson.Types.Parser (Creds m) parseCreds at' (Object m) = do id' <- m .: "id" let id'' = "http://graph.facebook.com/" `mappend` id' - name <- m .: "name" - email <- m .: "email" + name <- m .:? "name" + email <- m .:? "email" return $ Creds "facebook" id'' $ maybe id (\x -> (:) ("verifiedEmail", x)) email diff --git a/yesod-auth/Yesod/Auth/GoogleEmail.hs b/yesod-auth/Yesod/Auth/GoogleEmail.hs new file mode 100644 index 00000000..f1e5075d --- /dev/null +++ b/yesod-auth/Yesod/Auth/GoogleEmail.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +-- | Use an email address as an identifier via Google's OpenID login system. +-- +-- This backend will not use the OpenID identifier at all. It only uses OpenID +-- as a login system. 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). +module Yesod.Auth.GoogleEmail + ( authGoogleEmail + , forwardUrl + ) where + +import Yesod.Auth +import qualified Web.Authenticate.OpenId as OpenId +import Control.Monad.Attempt + +import Yesod.Form +import Yesod.Handler +import Yesod.Widget +import Yesod.Request +import Text.Blaze (toHtml) +import Data.Text (Text) +import qualified Yesod.Auth.Message as Msg +import qualified Data.Text as T + +forwardUrl :: AuthRoute +forwardUrl = PluginR "googleemail" ["forward"] + +authGoogleEmail :: YesodAuth m => AuthPlugin m +authGoogleEmail = + AuthPlugin "googleemail" dispatch login + where + complete = PluginR "googleemail" ["complete"] + name = "openid_identifier" + login tm = do + [whamlet| +

+ + +|] + dispatch "GET" ["forward"] = do + roid <- runInputGet $ iopt textField name + case roid of + Just oid -> do + render <- getUrlRender + toMaster <- getRouteToMaster + let complete' = render $ toMaster complete + res <- runAttemptT $ OpenId.getForwardUrl oid complete' Nothing + [ ("openid.ax.type.email", "http://schema.openid.net/contact/email") + , ("openid.ns.ax", "http://openid.net/srv/ax/1.0") + , ("openid.ns.ax.required", "email") + , ("openid.ax.mode", "fetch_request") + , ("openid.ax.required", "email") + , ("openid.ui.icon", "true") + ] + attempt + (\err -> do + setMessage $ toHtml $ show err + redirect RedirectTemporary $ toMaster LoginR + ) + (redirectText RedirectTemporary) + res + Nothing -> do + toMaster <- getRouteToMaster + setMessageI Msg.NoOpenID + redirect RedirectTemporary $ toMaster LoginR + dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues + dispatch "GET" ["complete"] = do + rr <- getRequest + completeHelper $ reqGetParams rr + dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues + dispatch "POST" ["complete"] = do + (posts, _) <- runRequestBody + completeHelper posts + dispatch _ _ = notFound + +completeHelper :: YesodAuth m => [(Text, Text)] -> GHandler Auth m () +completeHelper gets' = do + res <- runAttemptT $ OpenId.authenticate gets' + toMaster <- getRouteToMaster + let onFailure err = do + setMessage $ toHtml $ show err + redirect RedirectTemporary $ toMaster LoginR + let onSuccess (OpenId.Identifier ident, _) = do + memail <- lookupGetParam "openid.ext1.value.email" + case (memail, "https://www.google.com/accounts/o8/id" `T.isPrefixOf` ident) of + (Just email, True) -> setCreds True $ Creds "openid" email [] + (_, False) -> do + setMessage "Only Google login is supported" + redirect RedirectTemporary $ toMaster LoginR + (Nothing, _) -> do + setMessage "No email address provided" + redirect RedirectTemporary $ toMaster LoginR + attempt onFailure onSuccess res diff --git a/yesod-auth/Yesod/Auth/HashDB.hs b/yesod-auth/Yesod/Auth/HashDB.hs index 358c9345..5494d61b 100644 --- a/yesod-auth/Yesod/Auth/HashDB.hs +++ b/yesod-auth/Yesod/Auth/HashDB.hs @@ -98,10 +98,18 @@ class HashDBUser user where userPasswordHash :: user -> Maybe Text -- | Retrieve salt for password userPasswordSalt :: user -> Maybe Text - -- | Set hash and password + + -- | 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 @@ -118,7 +126,7 @@ saltedHash salt = -- 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 $ setUserHashAndSalt salt (saltedHash salt pwd) u + return $ setSaltAndPasswordHash salt (saltedHash salt pwd) u ---------------------------------------------------------------- @@ -256,6 +264,6 @@ User instance HashDBUser (UserGeneric backend) where userPasswordHash = Just . userPassword userPasswordSalt = Just . userSalt - setUserHashAndSalt s h u = u { userSalt = s + setSaltAndPasswordHash s h u = u { userSalt = s , userPassword = h } diff --git a/yesod-auth/Yesod/Auth/Message.hs b/yesod-auth/Yesod/Auth/Message.hs index c2d63645..5a154488 100644 --- a/yesod-auth/Yesod/Auth/Message.hs +++ b/yesod-auth/Yesod/Auth/Message.hs @@ -10,6 +10,8 @@ import Data.Text (Text) data AuthMessage = NoOpenID | LoginOpenID + | LoginGoogle + | LoginYahoo | Email | Password | Register @@ -37,6 +39,8 @@ data AuthMessage = defaultMessage :: AuthMessage -> Text defaultMessage NoOpenID = "No OpenID identifier found" defaultMessage LoginOpenID = "Login via OpenID" +defaultMessage LoginGoogle = "Login via Google" +defaultMessage LoginYahoo = "Login via Yahoo" defaultMessage Email = "Email" defaultMessage Password = "Password" defaultMessage Register = "Register" diff --git a/yesod-auth/Yesod/Auth/OpenId.hs b/yesod-auth/Yesod/Auth/OpenId.hs index d27e49a6..38891278 100644 --- a/yesod-auth/Yesod/Auth/OpenId.hs +++ b/yesod-auth/Yesod/Auth/OpenId.hs @@ -3,6 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} module Yesod.Auth.OpenId ( authOpenId + , authOpenIdExtended , forwardUrl ) where @@ -26,7 +27,10 @@ forwardUrl :: AuthRoute forwardUrl = PluginR "openid" ["forward"] authOpenId :: YesodAuth m => AuthPlugin m -authOpenId = +authOpenId = authOpenIdExtended [] + +authOpenIdExtended :: YesodAuth m => [(Text, Text)] -> AuthPlugin m +authOpenIdExtended extensionFields = AuthPlugin "openid" dispatch login where complete = PluginR "openid" ["complete"] @@ -39,6 +43,12 @@ authOpenId = padding-left: 18px; |] [QQ(whamlet)| + + +