#{t} + $if canEdit + + Edit + |] + provideRep $ return $ toJSON $ + case Map.lookup page content of + -- Our HTML representation sends a redirect if the page isn't + -- found, but our JSON representation just returns a JSON value + -- instead. + Nothing -> object ["error" .= ("Page not found" :: Text)] + Just (Textarea t) -> object ["content" .= t] + +getWikiEditR :: Texts -> WikiHandler Html +getWikiEditR page = do + canEdit <- lift $ canEditPage page + unless canEdit $ permissionDenied "You do not have permissions to edit this page." + + content <- getContent + let form = renderTable + $ areq textareaField "Content" (Map.lookup page content) + + -- We need to use lift here since the widget will be used below. + -- Practically speaking, this means that we'll be rendering form messages + -- using the master site's translation functions. + ((res, widget), enctype) <- lift $ runFormPost form + + case res of + FormSuccess t -> do + putContent page t + setMessage "Content updated" + redirect $ WikiEditR page + _ -> do + toParent <- getRouteToParent + lift $ defaultLayout + [whamlet| + + Read page + + + ^{widget} + + + Update page + |] + +postWikiEditR :: Texts -> WikiHandler Html +postWikiEditR = getWikiEditR diff --git a/demo/subsite/WikiRoutes.hs b/demo/subsite/WikiRoutes.hs new file mode 100644 index 00000000..f22c0222 --- /dev/null +++ b/demo/subsite/WikiRoutes.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +-- | Define our Wiki data type, routes, and the YesodWiki typeclass. Due to the +-- GHC stage restriction, the routes must be declared in a separate module from +-- our dispatch instance. +module WikiRoutes where + +import Control.Monad (liftM) +import Control.Monad.IO.Class (MonadIO) +import Data.IORef (IORef, newIORef) +import Data.Map (Map, empty) +import Yesod + +-- | Simple Wiki datatype: just store a Map from Wiki path to the contents of +-- the page. +data Wiki = Wiki + { wikiContent :: IORef (Map Texts Textarea) + } + +-- | A typeclass that all master sites that want a Wiki must implement. A +-- master must be able to render form messages, as we use yesod-forms for +-- processing user input. +class (RenderMessage master FormMessage, Yesod master) => YesodWiki master where + -- | Write protection. By default, no protection. + canEditPage :: Texts -> HandlerT master IO Bool + canEditPage _ = return True + +-- | Define our routes. We'll have a homepage that lists all of the pages, a +-- read route for reading a page, and an edit route. +mkYesodSubData "Wiki" [parseRoutes| +/ WikiHomeR GET +/read/*Texts WikiReadR GET +/edit/*Texts WikiEditR GET POST +|] + +-- | A convenience function for creating an empty Wiki. +newWiki :: MonadIO m => m Wiki +newWiki = Wiki `liftM` liftIO (newIORef empty) diff --git a/sources.txt b/sources.txt index 3830938b..7f19d275 100644 --- a/sources.txt +++ b/sources.txt @@ -1,12 +1,12 @@ ./yesod-routes ./yesod-core -./yesod-json ./yesod-static ./yesod-persistent ./yesod-newsfeed ./yesod-form ./yesod-auth ./yesod-sitemap -./yesod-default ./yesod-test +./yesod-bin ./yesod +https://github.com/yesodweb/persistent persistent1.2 diff --git a/yesod-auth/Yesod/Auth.hs b/yesod-auth/Yesod/Auth.hs index 8c560c70..19448c75 100644 --- a/yesod-auth/Yesod/Auth.hs +++ b/yesod-auth/Yesod/Auth.hs @@ -1,4 +1,6 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -15,6 +17,8 @@ module Yesod.Auth , AuthPlugin (..) , getAuth , YesodAuth (..) + , YesodAuthPersist + , AuthEntity -- * Plugin interface , Creds (..) , setCreds @@ -26,11 +30,14 @@ module Yesod.Auth , requireAuth -- * Exception , AuthException (..) + -- * Helper + , AuthHandler ) where -import Control.Monad (when) +import Control.Monad (when) import Control.Monad.Trans.Maybe +import Yesod.Auth.Routes import Data.Aeson import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode) @@ -39,31 +46,28 @@ import qualified Data.Text as T import qualified Data.HashMap.Lazy as Map import Network.HTTP.Conduit (Manager) -import Language.Haskell.TH.Syntax hiding (lift) - import qualified Network.Wai as W import Text.Hamlet (shamlet) import Yesod.Core import Yesod.Persist -import Yesod.Json import Yesod.Auth.Message (AuthMessage, defaultMessage) import qualified Yesod.Auth.Message as Msg import Yesod.Form (FormMessage) import Data.Typeable (Typeable) import Control.Exception (Exception) -data Auth = Auth - type AuthRoute = Route Auth +type AuthHandler master a = YesodAuth master => HandlerT Auth (HandlerT master IO) a + type Method = Text type Piece = Text data AuthPlugin master = AuthPlugin { apName :: Text - , apDispatch :: Method -> [Piece] -> GHandler Auth master () - , apLogin :: forall sub. (Route Auth -> Route master) -> GWidget sub master () + , apDispatch :: Method -> [Piece] -> AuthHandler master () + , apLogin :: (Route Auth -> Route master) -> WidgetT master IO () } getAuth :: a -> Auth @@ -88,23 +92,25 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage logoutDest :: master -> Route master -- | Determine the ID associated with the set of credentials. - getAuthId :: Creds master -> GHandler sub master (Maybe (AuthId master)) + getAuthId :: Creds master -> HandlerT master IO (Maybe (AuthId master)) -- | Which authentication backends to use. authPlugins :: master -> [AuthPlugin master] -- | What to show on the login page. - loginHandler :: GHandler Auth master RepHtml - loginHandler = defaultLayout $ do - setTitleI Msg.LoginTitle - tm <- lift getRouteToMaster - master <- lift getYesod - mapM_ (flip apLogin tm) (authPlugins master) + loginHandler :: AuthHandler master RepHtml + loginHandler = do + tp <- getRouteToParent + lift $ defaultLayout $ do + setTitleI Msg.LoginTitle + master <- getYesod + mapM_ (flip apLogin tp) (authPlugins master) -- | Used for i18n of messages provided by this package. renderAuthMessage :: master -> [Text] -- ^ languages - -> AuthMessage -> Text + -> AuthMessage + -> Text renderAuthMessage _ _ = defaultMessage -- | After login and logout, redirect to the referring page, instead of @@ -120,11 +126,11 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage -- | Called on a successful login. By default, calls -- @setMessageI NowLoggedIn@. - onLogin :: GHandler sub master () + onLogin :: HandlerT master IO () onLogin = setMessageI Msg.NowLoggedIn -- | Called on logout. By default, does nothing - onLogout :: GHandler sub master () + onLogout :: HandlerT master IO () onLogout = return () -- | Retrieves user credentials, if user is authenticated. @@ -135,8 +141,20 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage -- especially useful for creating an API to be accessed via some means -- other than a browser. -- - -- Since 1.1.2 - maybeAuthId :: GHandler sub master (Maybe (AuthId master)) + -- Since 1.2.0 + maybeAuthId :: HandlerT master IO (Maybe (AuthId master)) + + default maybeAuthId + :: ( YesodAuth master + , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val + , b ~ YesodPersistBackend master + , Key val ~ AuthId master + , PersistStore (b (HandlerT master IO)) + , PersistEntity val + , YesodPersist master + , Typeable val + ) + => HandlerT master IO (Maybe (AuthId master)) maybeAuthId = defaultMaybeAuthId credsKey :: Text @@ -144,50 +162,80 @@ credsKey = "_ID" -- | Retrieves user credentials from the session, if user is authenticated. -- +-- This function does /not/ confirm that the credentials are valid, see +-- 'maybeAuthIdRaw' for more information. +-- -- Since 1.1.2 -defaultMaybeAuthId :: YesodAuth master - => GHandler sub master (Maybe (AuthId master)) +defaultMaybeAuthId + :: ( YesodAuth master + , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val + , b ~ YesodPersistBackend master + , Key val ~ AuthId master + , PersistStore (b (HandlerT master IO)) + , PersistEntity val + , YesodPersist master + , Typeable val + ) => HandlerT master IO (Maybe (AuthId master)) defaultMaybeAuthId = do ms <- lookupSession credsKey case ms of Nothing -> return Nothing - Just s -> return $ fromPathPiece s + Just s -> + case fromPathPiece s of + Nothing -> return Nothing + Just aid -> fmap (fmap entityKey) $ cachedAuth aid -mkYesodSub "Auth" - [ ClassP ''YesodAuth [VarT $ mkName "master"] - ] -#define STRINGS *Texts - [parseRoutes| -/check CheckR GET -/login LoginR GET -/logout LogoutR GET POST -/page/#Text/STRINGS PluginR -|] +cachedAuth :: ( YesodAuth master + , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val + , b ~ YesodPersistBackend master + , Key val ~ AuthId master + , PersistStore (b (HandlerT master IO)) + , PersistEntity val + , YesodPersist master + , Typeable val + ) => AuthId master -> HandlerT master IO (Maybe (Entity val)) +cachedAuth aid = runMaybeT $ do + a <- MaybeT $ fmap unCachedMaybeAuth + $ cached + $ fmap CachedMaybeAuth + $ runDB + $ get aid + return $ Entity aid a -- | Sets user credentials for the session after checking them with authentication backends. setCreds :: YesodAuth master => Bool -- ^ if HTTP redirects should be done -> Creds master -- ^ new credentials - -> GHandler sub master () + -> HandlerT master IO () setCreds doRedirects creds = do y <- getYesod maid <- getAuthId creds case maid of - Nothing -> - when doRedirects $ do + Nothing -> when doRedirects $ do case authRoute y of - Nothing -> do rh <- defaultLayout $ toWidget [shamlet| -$newline never -Invalid login -|] - sendResponse rh - Just ar -> do setMessageI Msg.InvalidLogin - redirect ar + Nothing -> do + res <- selectRep $ do + provideRep $ defaultLayout $ toWidget [shamlet|Invalid login|] + provideRep $ return $ object ["message" .= ("Invalid Login" :: Text)] + sendResponse res + Just ar -> do + res <- selectRep $ do + provideRepType typeHtml $ do + setMessageI Msg.InvalidLogin + _ <- redirect ar + return () + provideRep $ return $ object ["message" .= ("Invalid Login" :: Text)] + sendResponse res Just aid -> do setSession credsKey $ toPathPiece aid when doRedirects $ do onLogin - redirectUltDest $ loginDest y + res <- selectRep $ do + provideRepType typeHtml $ do + _ <- redirectUltDest $ loginDest y + return () + provideRep $ return $ object ["message" .= ("Login Successful" :: Text)] + sendResponse res -- | Clears current user credentials for the session. -- @@ -202,12 +250,12 @@ clearCreds doRedirects = do onLogout redirectUltDest $ logoutDest y -getCheckR :: YesodAuth master => GHandler Auth master RepHtmlJson -getCheckR = do +getCheckR :: AuthHandler master TypedContent +getCheckR = lift $ do creds <- maybeAuthId defaultLayoutJson (do setTitle "Authentication Status" - toWidget $ html' creds) (jsonCreds creds) + toWidget $ html' creds) (return $ jsonCreds creds) where html' creds = [shamlet| @@ -223,25 +271,32 @@ $nothing [ (T.pack "logged_in", Bool $ maybe False (const True) creds) ] -setUltDestReferer' :: YesodAuth master => GHandler sub master () -setUltDestReferer' = do +setUltDestReferer' :: AuthHandler master () +setUltDestReferer' = lift $ do master <- getYesod when (redirectToReferer master) setUltDestReferer -getLoginR :: YesodAuth master => GHandler Auth master RepHtml +getLoginR :: AuthHandler master RepHtml getLoginR = setUltDestReferer' >> loginHandler -getLogoutR :: YesodAuth master => GHandler Auth master () -getLogoutR = do - tm <- getRouteToMaster - setUltDestReferer' >> redirectToPost (tm LogoutR) +getLogoutR :: AuthHandler master () +getLogoutR = setUltDestReferer' >> redirectToPost LogoutR +<<<<<<< HEAD postLogoutR :: YesodAuth master => GHandler Auth master () postLogoutR = clearCreds True +======= +postLogoutR :: AuthHandler master () +postLogoutR = lift $ do + y <- getYesod + deleteSession credsKey + onLogout + redirectUltDest $ logoutDest y +>>>>>>> origin/yesod1.2 -handlePluginR :: YesodAuth master => Text -> [Text] -> GHandler Auth master () +handlePluginR :: Text -> [Text] -> AuthHandler master () handlePluginR plugin pieces = do - master <- getYesod + master <- lift getYesod env <- waiRequest let method = decodeUtf8With lenientDecode $ W.requestMethod env case filter (\x -> apName x == plugin) (authPlugins master) of @@ -249,45 +304,58 @@ handlePluginR plugin pieces = do ap:_ -> apDispatch ap method pieces maybeAuth :: ( YesodAuth master -#if MIN_VERSION_persistent(1, 1, 0) - , PersistMonadBackend (b (GHandler sub master)) ~ PersistEntityBackend val + , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend val , b ~ YesodPersistBackend master , Key val ~ AuthId master - , PersistStore (b (GHandler sub master)) -#else - , b ~ YesodPersistBackend master - , b ~ PersistEntityBackend val - , Key b val ~ AuthId master - , PersistStore b (GHandler sub master) -#endif + , PersistStore (b (HandlerT master IO)) , PersistEntity val , YesodPersist master - ) => GHandler sub master (Maybe (Entity val)) + , Typeable val + ) => HandlerT master IO (Maybe (Entity val)) maybeAuth = runMaybeT $ do - aid <- MaybeT $ maybeAuthId - a <- MaybeT $ runDB $ get aid - return $ Entity aid a + aid <- MaybeT maybeAuthId + MaybeT $ cachedAuth aid -requireAuthId :: YesodAuth master => GHandler sub master (AuthId master) +newtype CachedMaybeAuth val = CachedMaybeAuth { unCachedMaybeAuth :: Maybe val } + deriving Typeable + +-- | Constraint which states that the given site is an instance of @YesodAuth@ +-- and that its @AuthId@ is in fact a persistent @Key@ for the given value. +-- This is the common case in Yesod, and means that you can easily look up the +-- full informatin on a given user. +-- +-- Since 1.2.0 +type YesodAuthPersist master = + ( YesodAuth master + , PersistMonadBackend (YesodPersistBackend master (HandlerT master IO)) + ~ PersistEntityBackend (AuthEntity master) + , Key (AuthEntity master) ~ AuthId master + , PersistStore (YesodPersistBackend master (HandlerT master IO)) + , PersistEntity (AuthEntity master) + , YesodPersist master + , Typeable (AuthEntity master) + ) + +-- | If the @AuthId@ for a given site is a persistent ID, this will give the +-- value for that entity. E.g.: +-- +-- > type AuthId MySite = UserId +-- > AuthEntity MySite ~ User +-- +-- Since 1.2.0 +type AuthEntity master = KeyEntity (AuthId master) + +-- | Similar to 'maybeAuthId', but redirects to a login page if user is not +-- authenticated. +-- +-- Since 1.1.0 +requireAuthId :: YesodAuthPersist master => HandlerT master IO (AuthId master) requireAuthId = maybeAuthId >>= maybe redirectLogin return -requireAuth :: ( YesodAuth master - , b ~ YesodPersistBackend master -#if MIN_VERSION_persistent(1, 1, 0) - , PersistMonadBackend (b (GHandler sub master)) ~ PersistEntityBackend val - , Key val ~ AuthId master - , PersistStore (b (GHandler sub master)) -#else - , b ~ PersistEntityBackend val - , Key b val ~ AuthId master - , PersistStore b (GHandler sub master) -#endif - , PersistEntity val - , YesodPersist master - ) => GHandler sub master (Entity val) +requireAuth :: YesodAuthPersist master => HandlerT master IO (Entity (AuthEntity master)) requireAuth = maybeAuth >>= maybe redirectLogin return -redirectLogin :: Yesod master => GHandler sub master a +redirectLogin :: Yesod master => HandlerT master IO a redirectLogin = do y <- getYesod setUltDestCurrent @@ -302,3 +370,6 @@ data AuthException = InvalidBrowserIDAssertion | InvalidFacebookResponse deriving (Show, Typeable) instance Exception AuthException + +instance YesodAuth master => YesodSubDispatch Auth (HandlerT master IO) where + yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth) diff --git a/yesod-auth/Yesod/Auth/BrowserId.hs b/yesod-auth/Yesod/Auth/BrowserId.hs index 9aab88ac..402453b4 100644 --- a/yesod-auth/Yesod/Auth/BrowserId.hs +++ b/yesod-auth/Yesod/Auth/BrowserId.hs @@ -1,10 +1,14 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE RecordWildCards #-} module Yesod.Auth.BrowserId ( authBrowserId - , authBrowserIdAudience , createOnClick + , def + , BrowserIdSettings + , bisAudience + , bisLazyLoad ) where import Yesod.Auth @@ -14,14 +18,13 @@ import Yesod.Core import Text.Hamlet (hamlet) import qualified Data.Text as T import Data.Maybe (fromMaybe) -import Control.Monad.IO.Class (liftIO) -import Control.Monad (when) +import Control.Monad (when, unless) import Control.Exception (throwIO) import Text.Julius (julius, rawJS) -import Data.Aeson (toJSON) import Network.URI (uriPath, parseURI) import Data.FileEmbed (embedFile) import Data.ByteString (ByteString) +import Data.Default pid :: Text pid = "browserid" @@ -29,38 +32,50 @@ pid = "browserid" complete :: Route Auth complete = PluginR pid [] --- | Log into browser ID with an audience value determined from the 'approot'. -authBrowserId :: YesodAuth m => AuthPlugin m -authBrowserId = helper Nothing +-- | A settings type for various configuration options relevant to BrowserID. +-- +-- See: +-- +-- Since 1.2.0 +data BrowserIdSettings = BrowserIdSettings + { bisAudience :: Maybe Text + -- ^ BrowserID audience value. If @Nothing@, will be extracted based on the + -- approot. + -- + -- Default: @Nothing@ + -- + -- Since 1.2.0 + , bisLazyLoad :: Bool + -- ^ Use asynchronous Javascript loading for the BrowserID JS file. + -- + -- Default: @True@. + -- + -- Since 1.2.0 + } --- | Log into browser ID with the given audience value. Note that this must be --- your actual hostname, or login will fail. -authBrowserIdAudience - :: YesodAuth m - => Text -- ^ audience - -> AuthPlugin m -authBrowserIdAudience = helper . Just +instance Default BrowserIdSettings where + def = BrowserIdSettings + { bisAudience = Nothing + , bisLazyLoad = True + } -helper :: YesodAuth m - => Maybe Text -- ^ audience - -> AuthPlugin m -helper maudience = AuthPlugin +authBrowserId :: YesodAuth m => BrowserIdSettings -> AuthPlugin m +authBrowserId bis@BrowserIdSettings {..} = AuthPlugin { apName = pid , apDispatch = \m ps -> case (m, ps) of ("GET", [assertion]) -> do - master <- getYesod + master <- lift getYesod audience <- - case maudience of + case bisAudience of Just a -> return a Nothing -> do - tm <- getRouteToMaster r <- getUrlRender - return $ T.takeWhile (/= '/') $ stripScheme $ r $ tm LoginR + return $ T.takeWhile (/= '/') $ stripScheme $ r LoginR memail <- lift $ checkAssertion audience assertion (authHttpManager master) case memail of Nothing -> liftIO $ throwIO InvalidBrowserIDAssertion - Just email -> setCreds True Creds + Just email -> lift $ setCreds True Creds { credsPlugin = pid , credsIdent = email , credsExtra = [] @@ -72,12 +87,10 @@ helper maudience = AuthPlugin (_, []) -> badMethod _ -> notFound , apLogin = \toMaster -> do - onclick <- createOnClick toMaster + onclick <- createOnClick bis toMaster - autologin <- fmap (== Just "true") $ lift $ lookupGetParam "autologin" - when autologin $ toWidget [julius| -#{rawJS onclick}(); -|] + autologin <- fmap (== Just "true") $ lookupGetParam "autologin" + when autologin $ toWidget [julius|#{rawJS onclick}();|] toWidget [hamlet| $newline never @@ -92,29 +105,45 @@ $newline never -- | Generates a function to handle on-click events, and returns that function -- name. -createOnClick :: (Route Auth -> Route master) -> GWidget sub master Text -createOnClick toMaster = do - addScriptRemote browserIdJs - onclick <- lift newIdent - render <- lift getUrlRender +createOnClick :: BrowserIdSettings + -> (Route Auth -> Route master) + -> WidgetT master IO Text +createOnClick BrowserIdSettings {..} toMaster = do + unless bisLazyLoad $ addScriptRemote browserIdJs + onclick <- newIdent + render <- getUrlRender let login = toJSON $ getPath $ render (toMaster LoginR) toWidget [julius| function #{rawJS onclick}() { - navigator.id.watch({ - onlogin: function (assertion) { - if (assertion) { - document.location = "@{toMaster complete}/" + assertion; - } - }, - onlogout: function () {} - }); - navigator.id.request({ - returnTo: #{login} + "?autologin=true" - }); + if (navigator.id) { + navigator.id.watch({ + onlogin: function (assertion) { + if (assertion) { + document.location = "@{toMaster complete}/" + assertion; + } + }, + onlogout: function () {} + }); + navigator.id.request({ + returnTo: #{login} + "?autologin=true" + }); + } + else { + alert("Loading, please try again"); + } } |] + when bisLazyLoad $ toWidget [julius| + (function(){ + var bid = document.createElement("script"); + bid.async = true; + bid.src = #{toJSON browserIdJs}; + var s = document.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(bid, s); + })(); + |] - autologin <- fmap (== Just "true") $ lift $ lookupGetParam "autologin" + autologin <- fmap (== Just "true") $ lookupGetParam "autologin" when autologin $ toWidget [julius|#{rawJS onclick}();|] return onclick where diff --git a/yesod-auth/Yesod/Auth/Dummy.hs b/yesod-auth/Yesod/Auth/Dummy.hs index 7ba931e5..9670f709 100644 --- a/yesod-auth/Yesod/Auth/Dummy.hs +++ b/yesod-auth/Yesod/Auth/Dummy.hs @@ -9,17 +9,16 @@ module Yesod.Auth.Dummy import Yesod.Auth import Yesod.Form (runInputPost, textField, ireq) -import Yesod.Handler (notFound) import Text.Hamlet (hamlet) -import Yesod.Widget (toWidget) +import Yesod.Core authDummy :: YesodAuth m => AuthPlugin m authDummy = AuthPlugin "dummy" dispatch login where dispatch "POST" [] = do - ident <- runInputPost $ ireq textField "ident" - setCreds True $ Creds "dummy" ident [] + ident <- lift $ runInputPost $ ireq textField "ident" + lift $ setCreds True $ Creds "dummy" ident [] dispatch _ _ = notFound url = PluginR "dummy" [] login authToMaster = diff --git a/yesod-auth/Yesod/Auth/Email.hs b/yesod-auth/Yesod/Auth/Email.hs index 2fe5d7e7..5b383cc3 100644 --- a/yesod-auth/Yesod/Auth/Email.hs +++ b/yesod-auth/Yesod/Auth/Email.hs @@ -1,6 +1,7 @@ {-# LANGUAGE QuasiQuotes, TypeFamilies #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE PatternGuards #-} module Yesod.Auth.Email ( -- * Plugin authEmail @@ -10,33 +11,40 @@ module Yesod.Auth.Email -- * Routes , loginR , registerR + , forgotPasswordR , setpassR , isValidPass + -- * Types + , Email + , VerKey + , VerUrl + , SaltedPass + , VerStatus + , Identifier ) where import Network.Mail.Mime (randomString) import Yesod.Auth import System.Random -import Control.Monad (when) -import Control.Applicative ((<$>), (<*>)) import Data.Digest.Pure.MD5 -import qualified Data.Text.Lazy as T import qualified Data.Text as TS -import Data.Text.Lazy.Encoding (encodeUtf8) +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TLE +import Data.Text.Encoding (encodeUtf8, decodeUtf8With) +import Data.Text.Encoding.Error (lenientDecode) import Data.Text (Text) +import Yesod.Core import qualified Crypto.PasswordStore as PS -import qualified Data.Text.Encoding as DTE - -import Yesod.Form -import Yesod.Handler -import Yesod.Content -import Yesod.Core (PathPiece, fromPathPiece, whamlet, defaultLayout, setTitleI, toPathPiece) -import Control.Monad.IO.Class (liftIO) +import qualified Text.Email.Validate import qualified Yesod.Auth.Message as Msg +import Control.Applicative ((<$>), (<*>)) +import Yesod.Form +import Control.Monad (when) -loginR, registerR, setpassR :: AuthRoute +loginR, registerR, forgotPasswordR, setpassR :: AuthRoute loginR = PluginR "email" ["login"] registerR = PluginR "email" ["register"] +forgotPasswordR = PluginR "email" ["forgot-password"] setpassR = PluginR "email" ["set-password"] verify :: Text -> Text -> AuthRoute -- FIXME @@ -48,33 +56,86 @@ type VerUrl = Text type SaltedPass = Text type VerStatus = Bool +-- | An Identifier generalizes an email address to allow users to log in with +-- some other form of credentials (e.g., username). +-- +-- Note that any of these other identifiers must not be valid email addresses. +-- +-- Since 1.2.0 +type Identifier = Text + -- | Data stored in a database for each e-mail address. -data EmailCreds m = EmailCreds - { emailCredsId :: AuthEmailId m - , emailCredsAuthId :: Maybe (AuthId m) +data EmailCreds site = EmailCreds + { emailCredsId :: AuthEmailId site + , emailCredsAuthId :: Maybe (AuthId site) , emailCredsStatus :: VerStatus , emailCredsVerkey :: Maybe VerKey + , emailCredsEmail :: Email } -class (YesodAuth m, PathPiece (AuthEmailId m)) => YesodAuthEmail m where - type AuthEmailId m +class (YesodAuth site, PathPiece (AuthEmailId site)) => YesodAuthEmail site where + type AuthEmailId site - addUnverified :: Email -> VerKey -> GHandler Auth m (AuthEmailId m) - sendVerifyEmail :: Email -> VerKey -> VerUrl -> GHandler Auth m () - getVerifyKey :: AuthEmailId m -> GHandler Auth m (Maybe VerKey) - setVerifyKey :: AuthEmailId m -> VerKey -> GHandler Auth m () - verifyAccount :: AuthEmailId m -> GHandler Auth m (Maybe (AuthId m)) - getPassword :: AuthId m -> GHandler Auth m (Maybe SaltedPass) - setPassword :: AuthId m -> SaltedPass -> GHandler Auth m () - getEmailCreds :: Email -> GHandler Auth m (Maybe (EmailCreds m)) - getEmail :: AuthEmailId m -> GHandler Auth m (Maybe Email) + -- | Add a new email address to the database, but indicate that the address + -- has not yet been verified. + -- + -- Since 1.1.0 + addUnverified :: Email -> VerKey -> HandlerT site IO (AuthEmailId site) + + -- | Send an email to the given address to verify ownership. + -- + -- Since 1.1.0 + sendVerifyEmail :: Email -> VerKey -> VerUrl -> HandlerT site IO () + + -- | Get the verification key for the given email ID. + -- + -- Since 1.1.0 + getVerifyKey :: AuthEmailId site -> HandlerT site IO (Maybe VerKey) + + -- | Set the verification key for the given email ID. + -- + -- Since 1.1.0 + setVerifyKey :: AuthEmailId site -> VerKey -> HandlerT site IO () + + -- | Verify the email address on the given account. + -- + -- Since 1.1.0 + verifyAccount :: AuthEmailId site -> HandlerT site IO (Maybe (AuthId site)) + + -- | Get the salted password for the given account. + -- + -- Since 1.1.0 + getPassword :: AuthId site -> HandlerT site IO (Maybe SaltedPass) + + -- | Set the salted password for the given account. + -- + -- Since 1.1.0 + setPassword :: AuthId site -> SaltedPass -> HandlerT site IO () + + -- | Get the credentials for the given @Identifier@, which may be either an + -- email address or some other identification (e.g., username). + -- + -- Since 1.2.0 + getEmailCreds :: Identifier -> HandlerT site IO (Maybe (EmailCreds site)) + + -- | Get the email address for the given email ID. + -- + -- Since 1.1.0 + getEmail :: AuthEmailId site -> HandlerT site IO (Maybe Email) -- | Generate a random alphanumeric string. - randomKey :: m -> IO Text + -- + -- Since 1.1.0 + randomKey :: site -> IO Text randomKey _ = do stdgen <- newStdGen return $ TS.pack $ fst $ randomString 10 stdgen + -- | Route to send user to after password has been set correctly. + -- + -- Since 1.2.0 + afterPasswordRoute :: site -> Route site + authEmail :: YesodAuthEmail m => AuthPlugin m authEmail = AuthPlugin "email" dispatch $ \tm -> @@ -98,6 +159,8 @@ $newline never where dispatch "GET" ["register"] = getRegisterR >>= sendResponse dispatch "POST" ["register"] = postRegisterR >>= sendResponse + dispatch "GET" ["forgot-password"] = getForgotPasswordR >>= sendResponse + dispatch "POST" ["forgot-password"] = postForgotPasswordR >>= sendResponse dispatch "GET" ["verify", eid, verkey] = case fromPathPiece eid of Nothing -> notFound @@ -107,113 +170,157 @@ $newline never dispatch "POST" ["set-password"] = postPasswordR >>= sendResponse dispatch _ _ = notFound -getRegisterR :: YesodAuthEmail master => GHandler Auth master RepHtml +getRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html getRegisterR = do - toMaster <- getRouteToMaster email <- newIdent - defaultLayout $ do + tp <- getRouteToParent + lift $ defaultLayout $ do setTitleI Msg.RegisterLong [whamlet| -$newline never -_{Msg.EnterEmail} - - _{Msg.Email} - - -|] + _{Msg.EnterEmail} + + + _{Msg.Email}: + + _{Msg.Register} + |] -postRegisterR :: YesodAuthEmail master => GHandler Auth master RepHtml -postRegisterR = do - y <- getYesod - email <- runInputPost $ ireq emailField "email" - mecreds <- getEmailCreds email - (lid, verKey) <- - case mecreds of - Just (EmailCreds lid _ _ (Just key)) -> return (lid, key) - Just (EmailCreds lid _ _ Nothing) -> do - key <- liftIO $ randomKey y - setVerifyKey lid key - return (lid, key) +registerHelper :: YesodAuthEmail master + => Bool -- ^ allow usernames? + -> Route Auth + -> HandlerT Auth (HandlerT master IO) Html +registerHelper allowUsername dest = do + y <- lift getYesod + midentifier <- lookupPostParam "email" + identifier <- + case midentifier of Nothing -> do + lift $ setMessageI Msg.NoIdentifierProvided + redirect dest + Just x + | Just x' <- Text.Email.Validate.canonicalizeEmail (encodeUtf8 x) -> + return $ decodeUtf8With lenientDecode x' + | allowUsername -> return $ TS.strip x + | otherwise -> do + lift $ setMessageI Msg.InvalidEmailAddress + redirect dest + 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 - lid <- addUnverified email key - return (lid, key) + 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 - tm <- getRouteToMaster - let verUrl = render $ tm $ verify (toPathPiece lid) verKey - sendVerifyEmail email verKey verUrl - defaultLayout $ do + let verUrl = render $ verify (toPathPiece lid) verKey + lift $ sendVerifyEmail email verKey verUrl + lift $ defaultLayout $ do setTitleI Msg.ConfirmationEmailSentTitle + [whamlet|_{Msg.ConfirmationEmailSent identifier}|] + +postRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html +postRegisterR = registerHelper False registerR + +getForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html +getForgotPasswordR = do + tp <- getRouteToParent + email <- newIdent + lift $ defaultLayout $ do + setTitleI Msg.PasswordResetTitle [whamlet| -$newline never -_{Msg.ConfirmationEmailSent email} -|] + _{Msg.PasswordResetPrompt} + + + _{Msg.ProvideIdentifier} + + _{Msg.SendPasswordResetEmail} + |] + +postForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html +postForgotPasswordR = registerHelper True forgotPasswordR getVerifyR :: YesodAuthEmail m - => AuthEmailId m -> Text -> GHandler Auth m RepHtml + => AuthEmailId m -> Text -> HandlerT Auth (HandlerT m IO) Html getVerifyR lid key = do - realKey <- getVerifyKey lid - memail <- getEmail lid + realKey <- lift $ getVerifyKey lid + memail <- lift $ getEmail lid case (realKey == Just key, memail) of (True, Just email) -> do - muid <- verifyAccount lid + muid <- lift $ verifyAccount lid case muid of Nothing -> return () Just _uid -> do - setCreds False $ Creds "email" email [("verifiedEmail", email)] -- FIXME uid? - toMaster <- getRouteToMaster - setMessageI Msg.AddressVerified - redirect $ toMaster setpassR + lift $ setCreds False $ Creds "email-verify" email [("verifiedEmail", email)] -- FIXME uid? + lift $ setMessageI Msg.AddressVerified + redirect setpassR _ -> return () - defaultLayout $ do + lift $ defaultLayout $ do setTitleI Msg.InvalidKey [whamlet| $newline never _{Msg.InvalidKey} |] -postLoginR :: YesodAuthEmail master => GHandler Auth master () +postLoginR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) () postLoginR = do - (email, pass) <- runInputPost $ (,) - <$> ireq emailField "email" + (identifier, pass) <- lift $ runInputPost $ (,) + <$> ireq textField "email" <*> ireq textField "password" - mecreds <- getEmailCreds email + mecreds <- lift $ getEmailCreds identifier maid <- - case (mecreds >>= emailCredsAuthId, fmap emailCredsStatus mecreds) of - (Just aid, Just True) -> do - mrealpass <- getPassword aid + case ( mecreds >>= emailCredsAuthId + , emailCredsEmail <$> mecreds + , emailCredsStatus <$> mecreds + ) of + (Just aid, Just email, Just True) -> do + mrealpass <- lift $ getPassword aid case mrealpass of Nothing -> return Nothing Just realpass -> return $ if isValidPass pass realpass - then Just aid + then Just email else Nothing _ -> return Nothing + let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier case maid of - Just _aid -> - setCreds True $ Creds "email" email [("verifiedEmail", email)] -- FIXME aid? + Just email -> + lift $ setCreds True $ Creds + (if isEmail then "email" else "username") + email + [("verifiedEmail", email)] Nothing -> do - setMessageI Msg.InvalidEmailPass - toMaster <- getRouteToMaster - redirect $ toMaster LoginR + lift $ setMessageI $ + if isEmail + then Msg.InvalidEmailPass + else Msg.InvalidUsernamePass + redirect LoginR -getPasswordR :: YesodAuthEmail master => GHandler Auth master RepHtml +getPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html getPasswordR = do - toMaster <- getRouteToMaster - maid <- maybeAuthId + maid <- lift maybeAuthId pass1 <- newIdent pass2 <- newIdent case maid of Just _ -> return () Nothing -> do - setMessageI Msg.BadSetPass - redirect $ toMaster LoginR - defaultLayout $ do + lift $ setMessageI Msg.BadSetPass + redirect LoginR + tp <- getRouteToParent + lift $ defaultLayout $ do setTitleI Msg.SetPassTitle [whamlet| $newline never _{Msg.SetPass} - + @@ -227,50 +334,47 @@ $newline never - + |] -postPasswordR :: YesodAuthEmail master => GHandler Auth master () +postPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) () postPasswordR = do - (new, confirm) <- runInputPost $ (,) + (new, confirm) <- lift $ runInputPost $ (,) <$> ireq textField "new" <*> ireq textField "confirm" - toMaster <- getRouteToMaster - y <- getYesod when (new /= confirm) $ do - setMessageI Msg.PassMismatch - redirect $ toMaster setpassR - maid <- maybeAuthId + lift $ setMessageI Msg.PassMismatch + redirect setpassR + maid <- lift maybeAuthId aid <- case maid of Nothing -> do - setMessageI Msg.BadSetPass - redirect $ toMaster LoginR + lift $ setMessageI Msg.BadSetPass + redirect LoginR Just aid -> return aid salted <- liftIO $ saltPass new - setPassword aid salted - setMessageI Msg.PassUpdated - redirect $ loginDest y + lift $ do + y <- getYesod + setPassword aid salted + setMessageI Msg.PassUpdated + redirect $ afterPasswordRoute y saltLength :: Int saltLength = 5 -- | Salt a password with a randomly generated salt. saltPass :: Text -> IO Text -saltPass = fmap DTE.decodeUtf8 +saltPass = fmap (decodeUtf8With lenientDecode) . flip PS.makePassword 12 - . DTE.encodeUtf8 + . encodeUtf8 saltPass' :: String -> String -> String -saltPass' salt pass = - salt ++ show (md5 $ fromString $ salt ++ pass) - where - fromString = encodeUtf8 . T.pack +saltPass' salt pass = salt ++ show (md5 $ TLE.encodeUtf8 $ TL.pack $ salt ++ pass) isValidPass :: Text -- ^ cleartext password -> SaltedPass -- ^ salted password -> Bool isValidPass ct salted = - PS.verifyPassword (DTE.encodeUtf8 ct) (DTE.encodeUtf8 salted) || isValidPass' ct salted + PS.verifyPassword (encodeUtf8 ct) (encodeUtf8 salted) || isValidPass' ct salted isValidPass' :: Text -- ^ cleartext password -> SaltedPass -- ^ salted password diff --git a/yesod-auth/Yesod/Auth/GoogleEmail.hs b/yesod-auth/Yesod/Auth/GoogleEmail.hs index 8f06abe3..d9ce98fe 100644 --- a/yesod-auth/Yesod/Auth/GoogleEmail.hs +++ b/yesod-auth/Yesod/Auth/GoogleEmail.hs @@ -1,6 +1,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE RankNTypes #-} -- | 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 @@ -18,14 +19,7 @@ module Yesod.Auth.GoogleEmail import Yesod.Auth import qualified Web.Authenticate.OpenId as OpenId -import Yesod.Handler -import Yesod.Widget (whamlet) -import Yesod.Request -#if MIN_VERSION_blaze_html(0, 5, 0) -import Text.Blaze.Html (toHtml) -#else -import Text.Blaze (toHtml) -#endif +import Yesod.Core import Data.Text (Text) import qualified Yesod.Auth.Message as Msg import qualified Data.Text as T @@ -46,15 +40,11 @@ authGoogleEmail = where complete = PluginR pid ["complete"] login tm = - [whamlet| -$newline never -_{Msg.LoginGoogle} -|] + [whamlet|_{Msg.LoginGoogle}|] dispatch "GET" ["forward"] = do render <- getUrlRender - toMaster <- getRouteToMaster - let complete' = render $ toMaster complete - master <- getYesod + let complete' = render complete + master <- lift getYesod eres <- lift $ try $ OpenId.getForwardUrl googleIdent complete' Nothing [ ("openid.ax.type.email", "http://schema.openid.net/contact/email") , ("openid.ns.ax", "http://openid.net/srv/ax/1.0") @@ -66,7 +56,7 @@ $newline never either (\err -> do setMessage $ toHtml $ show (err :: SomeException) - redirect $ toMaster LoginR + redirect LoginR ) redirect eres @@ -80,23 +70,22 @@ $newline never completeHelper posts dispatch _ _ = notFound -completeHelper :: YesodAuth m => [(Text, Text)] -> GHandler Auth m () +completeHelper :: YesodAuth master => [(Text, Text)] -> AuthHandler master () completeHelper gets' = do - master <- getYesod + master <- lift getYesod eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master) - toMaster <- getRouteToMaster let onFailure err = do setMessage $ toHtml $ show (err :: SomeException) - redirect $ toMaster LoginR + redirect LoginR let onSuccess oir = do let OpenId.Identifier ident = OpenId.oirOpLocal oir 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 pid email [] + (Just email, True) -> lift $ setCreds True $ Creds pid email [] (_, False) -> do setMessage "Only Google login is supported" - redirect $ toMaster LoginR + redirect LoginR (Nothing, _) -> do setMessage "No email address provided" - redirect $ toMaster LoginR + redirect LoginR either onFailure onSuccess eres diff --git a/yesod-auth/Yesod/Auth/HashDB.hs b/yesod-auth/Yesod/Auth/HashDB.hs index 356ac62f..16117e2a 100644 --- a/yesod-auth/Yesod/Auth/HashDB.hs +++ b/yesod-auth/Yesod/Auth/HashDB.hs @@ -74,15 +74,13 @@ module Yesod.Auth.HashDB ) where import Yesod.Persist -import Yesod.Handler import Yesod.Form import Yesod.Auth -import Yesod.Widget (toWidget) +import Yesod.Core import Text.Hamlet (hamlet) import Control.Applicative ((<$>), (<*>)) import Control.Monad (replicateM,liftM) -import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Data.ByteString.Lazy.Char8 as BS (pack) import Data.Digest.Pure.SHA (sha1, showDigest) @@ -135,26 +133,15 @@ setPassword pwd u = do salt <- randomSalt -- | Given a user ID and password in plaintext, validate them against -- the database values. validateUser :: ( YesodPersist yesod -#if MIN_VERSION_persistent(1, 1, 0) , b ~ YesodPersistBackend yesod - , PersistMonadBackend (b (GHandler sub yesod)) ~ PersistEntityBackend user - , PersistUnique (b (GHandler sub yesod)) -#else - , b ~ YesodPersistBackend yesod - , b ~ PersistEntityBackend user - , PersistStore b (GHandler sub yesod) - , PersistUnique b (GHandler sub yesod) -#endif + , PersistMonadBackend (b (HandlerT yesod IO)) ~ PersistEntityBackend user + , PersistUnique (b (HandlerT yesod IO)) , PersistEntity user , HashDBUser user ) => -#if MIN_VERSION_persistent(1, 1, 0) Unique user -- ^ User unique identifier -#else - Unique user b -- ^ User unique identifier -#endif -> Text -- ^ Password in plaint-text - -> GHandler sub yesod Bool + -> HandlerT yesod IO Bool validateUser userID passwd = do -- Checks that hash and password match let validate u = do hash <- userPasswordHash u @@ -173,62 +160,38 @@ login = PluginR "hashdb" ["login"] -- username (whatever it might be) to unique user ID. postLoginR :: ( YesodAuth y, YesodPersist y , HashDBUser user, PersistEntity user -#if MIN_VERSION_persistent(1, 1, 0) , b ~ YesodPersistBackend y - , PersistMonadBackend (b (GHandler Auth y)) ~ PersistEntityBackend user - , PersistUnique (b (GHandler Auth y)) -#else - , b ~ YesodPersistBackend y - , b ~ PersistEntityBackend user - , PersistStore b (GHandler Auth y) - , PersistUnique b (GHandler Auth y) -#endif + , PersistMonadBackend (b (HandlerT y IO)) ~ PersistEntityBackend user + , PersistUnique (b (HandlerT y IO)) ) -#if MIN_VERSION_persistent(1, 1, 0) => (Text -> Maybe (Unique user)) -#else - => (Text -> Maybe (Unique user b)) -#endif - -> GHandler Auth y () + -> HandlerT Auth (HandlerT y IO) () postLoginR uniq = do - (mu,mp) <- runInputPost $ (,) + (mu,mp) <- lift $ runInputPost $ (,) <$> iopt textField "username" <*> iopt textField "password" - isValid <- fromMaybe (return False) + isValid <- lift $ fromMaybe (return False) (validateUser <$> (uniq =<< mu) <*> mp) if isValid - then setCreds True $ Creds "hashdb" (fromMaybe "" mu) [] + then lift $ setCreds True $ Creds "hashdb" (fromMaybe "" mu) [] else do setMessage "Invalid username/password" - toMaster <- getRouteToMaster - redirect $ toMaster LoginR + redirect LoginR -- | 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 -#if MIN_VERSION_persistent(1, 1, 0) , Key user ~ AuthId master , b ~ YesodPersistBackend master - , PersistMonadBackend (b (GHandler sub master)) ~ PersistEntityBackend user - , PersistUnique (b (GHandler sub master)) -#else - , Key b user ~ AuthId master - , b ~ YesodPersistBackend master - , b ~ PersistEntityBackend user - , PersistUnique b (GHandler sub master) - , PersistStore b (GHandler sub master) -#endif + , PersistMonadBackend (b (HandlerT master IO)) ~ PersistEntityBackend user + , PersistUnique (b (HandlerT master IO)) ) => (AuthRoute -> Route master) -- ^ your site's Auth Route -#if MIN_VERSION_persistent(1, 1, 0) -> (Text -> Maybe (Unique user)) -- ^ gets user ID -#else - -> (Text -> Maybe (Unique user b)) -- ^ gets user ID -#endif -> Creds master -- ^ the creds argument - -> GHandler sub master (Maybe (AuthId master)) + -> HandlerT master IO (Maybe (AuthId master)) getAuthIdHashDB authR uniq creds = do muid <- maybeAuthId case muid of @@ -250,18 +213,10 @@ getAuthIdHashDB authR uniq creds = do authHashDB :: ( YesodAuth m, YesodPersist m , HashDBUser user , PersistEntity user -#if MIN_VERSION_persistent(1, 1, 0) , b ~ YesodPersistBackend m - , PersistMonadBackend (b (GHandler Auth m)) ~ PersistEntityBackend user - , PersistUnique (b (GHandler Auth m))) + , PersistMonadBackend (b (HandlerT m IO)) ~ PersistEntityBackend user + , PersistUnique (b (HandlerT m IO))) => (Text -> Maybe (Unique user)) -> AuthPlugin m -#else - , b ~ YesodPersistBackend m - , b ~ PersistEntityBackend user - , PersistStore b (GHandler Auth m) - , PersistUnique b (GHandler Auth m)) - => (Text -> Maybe (Unique user b)) -> AuthPlugin m -#endif authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> toWidget [hamlet| $newline never diff --git a/yesod-auth/Yesod/Auth/Message.hs b/yesod-auth/Yesod/Auth/Message.hs index 97457238..085c496a 100644 --- a/yesod-auth/Yesod/Auth/Message.hs +++ b/yesod-auth/Yesod/Auth/Message.hs @@ -12,6 +12,8 @@ module Yesod.Auth.Message , norwegianBokmålMessage , japaneseMessage , finnishMessage + , chineseMessage + , spanishMessage ) where import Data.Monoid (mappend) @@ -47,6 +49,13 @@ data AuthMessage = | LoginTitle | PleaseProvideUsername | PleaseProvidePassword + | NoIdentifierProvided + | InvalidEmailAddress + | PasswordResetTitle + | ProvideIdentifier + | SendPasswordResetEmail + | PasswordResetPrompt + | InvalidUsernamePass -- | Defaults to 'englishMessage'. defaultMessage :: AuthMessage -> Text @@ -85,6 +94,13 @@ englishMessage NowLoggedIn = "You are now logged in" englishMessage LoginTitle = "Login" englishMessage PleaseProvideUsername = "Please fill in your username" englishMessage PleaseProvidePassword = "Please fill in your password" +englishMessage NoIdentifierProvided = "No email/username provided" +englishMessage InvalidEmailAddress = "Invalid email address provided" +englishMessage PasswordResetTitle = "Password Reset" +englishMessage ProvideIdentifier = "Email or Username" +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 InvalidUsernamePass = "Invalid username/password combination" portugueseMessage :: AuthMessage -> Text portugueseMessage NoOpenID = "Nenhum identificador OpenID encontrado" @@ -119,6 +135,54 @@ portugueseMessage NowLoggedIn = "Você acaba de entrar no site com sucesso!" portugueseMessage LoginTitle = "Entrar no site" portugueseMessage PleaseProvideUsername = "Por favor digite seu nome de usuário" portugueseMessage PleaseProvidePassword = "Por favor digite sua senha" +portugueseMessage NoIdentifierProvided = "Nenhum e-mail ou nome de usuário informado" +portugueseMessage InvalidEmailAddress = "Endereço de e-mail inválido informado" +portugueseMessage PasswordResetTitle = "Resetar senha" +portugueseMessage ProvideIdentifier = "E-mail ou nome de usuário" +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 InvalidUsernamePass = "Nome de usuário ou senha inválidos" + +spanishMessage :: AuthMessage -> Text +spanishMessage NoOpenID = "No se encuentra el identificador OpenID" +spanishMessage LoginOpenID = "Entrar utilizando OpenID" +spanishMessage LoginGoogle = "Entrar utilizando Google" +spanishMessage LoginYahoo = "Entrar utilizando Yahoo" +spanishMessage Email = "Correo electrónico" +spanishMessage Password = "Contraseña" +spanishMessage Register = "Registrarse" +spanishMessage RegisterLong = "Registrar una nueva cuenta" +spanishMessage EnterEmail = "Coloque su dirección de correo electrónico, y un correo de confirmación le será enviado a su cuenta." +spanishMessage ConfirmationEmailSentTitle = "La confirmación de correo ha sido enviada" +spanishMessage (ConfirmationEmailSent email) = + "Una confirmación de correo electrónico ha sido enviada a " `mappend` + email `mappend` + "." +spanishMessage AddressVerified = "Dirección verificada, por favor introduzca una contraseña" +spanishMessage InvalidKeyTitle = "Clave de verificación invalida" +spanishMessage InvalidKey = "Lo sentimos, pero esa clave de verificación es inválida." +spanishMessage InvalidEmailPass = "La combinación cuenta de correo/contraseña es inválida" +spanishMessage BadSetPass = "Debe acceder a la aplicación para modificar la contraseña" +spanishMessage SetPassTitle = "Modificar contraseña" +spanishMessage SetPass = "Actualizar nueva contraseña" +spanishMessage NewPass = "Nueva contraseña" +spanishMessage ConfirmPass = "Confirmar" +spanishMessage PassMismatch = "Las contraseñas no coinciden, inténtelo de nuevo" +spanishMessage PassUpdated = "Contraseña actualizada" +spanishMessage Facebook = "Entrar mediante Facebook" +spanishMessage LoginViaEmail = "Entrar mediante una cuenta de correo" +spanishMessage InvalidLogin = "Login inválido" +spanishMessage NowLoggedIn = "Usted ha ingresado al sitio" +spanishMessage LoginTitle = "Login" +spanishMessage PleaseProvideUsername = "Por favor escriba su nombre de usuario" +spanishMessage PleaseProvidePassword = "Por favor escriba su contraseña" +spanishMessage NoIdentifierProvided = "No ha indicado una cuenta de correo/nombre de usuario" +spanishMessage InvalidEmailAddress = "La cuenta de correo es inválida" +spanishMessage PasswordResetTitle = "Contraseña actualizada" +spanishMessage ProvideIdentifier = "Cuenta de correo o nombre de usuario" +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 InvalidUsernamePass = "Combinación de nombre de usuario/contraseña invalida" swedishMessage :: AuthMessage -> Text swedishMessage NoOpenID = "Fann ej OpenID identifierare" @@ -153,6 +217,14 @@ swedishMessage NowLoggedIn = "Du är nu inloggad" swedishMessage LoginTitle = "Logga in" swedishMessage PleaseProvideUsername = "Vänligen fyll i användarnamn" swedishMessage PleaseProvidePassword = "Vänligen fyll i lösenord" +swedishMessage NoIdentifierProvided = "Emailadress eller användarnamn saknas" +swedishMessage InvalidEmailAddress = "Ogiltig emailadress angiven" +swedishMessage PasswordResetTitle = "Återställning av lösenord" +swedishMessage ProvideIdentifier = "Epost eller användarnamn" +swedishMessage SendPasswordResetEmail = "Skicka email för återställning av lösenord" +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." +swedishMessage InvalidUsernamePass = "Ogiltig kombination av användarnamn och lösenord" germanMessage :: AuthMessage -> Text germanMessage NoOpenID = "Kein OpenID-Identifier gefunden" @@ -187,8 +259,13 @@ germanMessage NowLoggedIn = "Login erfolgreich" germanMessage LoginTitle = "Login" germanMessage PleaseProvideUsername = "Bitte Nutzername angeben" germanMessage PleaseProvidePassword = "Bitte Passwort angeben" - - +germanMessage NoIdentifierProvided = "Keine Email-Adresse oder kein Nutzername angegeben" +germanMessage InvalidEmailAddress = "Unzulässiger Email-Anbieter" +germanMessage PasswordResetTitle = "Passwort zurücksetzen" +germanMessage ProvideIdentifier = "Email-Adresse oder Nutzername" +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 InvalidUsernamePass = "Ungültige Kombination aus Nutzername und Passwort" frenchMessage :: AuthMessage -> Text frenchMessage NoOpenID = "Aucun fournisseur OpenID n'a été trouvé" @@ -223,6 +300,13 @@ frenchMessage NowLoggedIn = "Vous êtes maintenant connecté" frenchMessage LoginTitle = "Se connecter" frenchMessage PleaseProvideUsername = "Merci de renseigner votre nom d'utilisateur" frenchMessage PleaseProvidePassword = "Merci de spécifier un mot de passe" +frenchMessage NoIdentifierProvided = "No email/username provided" +frenchMessage InvalidEmailAddress = "Invalid email address provided" +frenchMessage PasswordResetTitle = "Password Reset" +frenchMessage ProvideIdentifier = "Email or Username" +frenchMessage SendPasswordResetEmail = "Send password reset email" +frenchMessage PasswordResetPrompt = "Enter your e-mail address or username below, and a password reset e-mail will be sent to you." +frenchMessage InvalidUsernamePass = "Invalid username/password combination" norwegianBokmålMessage :: AuthMessage -> Text norwegianBokmålMessage NoOpenID = "Ingen OpenID-identifiserer funnet" @@ -257,6 +341,13 @@ norwegianBokmålMessage NowLoggedIn = "Du er nå logget inn" norwegianBokmålMessage LoginTitle = "Logg inn" norwegianBokmålMessage PleaseProvideUsername = "Vennligst fyll inn ditt brukernavn" norwegianBokmålMessage PleaseProvidePassword = "Vennligst fyll inn ditt passord" +norwegianBokmålMessage NoIdentifierProvided = "No email/username provided" +norwegianBokmålMessage InvalidEmailAddress = "Invalid email address provided" +norwegianBokmålMessage PasswordResetTitle = "Password Reset" +norwegianBokmålMessage ProvideIdentifier = "Email or Username" +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 InvalidUsernamePass = "Invalid username/password combination" japaneseMessage :: AuthMessage -> Text japaneseMessage NoOpenID = "OpenID識別子がありません" @@ -291,6 +382,13 @@ japaneseMessage NowLoggedIn = "ログインしました" japaneseMessage LoginTitle = "ログイン" japaneseMessage PleaseProvideUsername = "ユーザ名を入力してください" japaneseMessage PleaseProvidePassword = "パスワードを入力してください" +japaneseMessage NoIdentifierProvided = "No email/username provided" +japaneseMessage InvalidEmailAddress = "Invalid email address provided" +japaneseMessage PasswordResetTitle = "Password Reset" +japaneseMessage ProvideIdentifier = "Email or Username" +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 InvalidUsernamePass = "Invalid username/password combination" finnishMessage :: AuthMessage -> Text finnishMessage NoOpenID = "OpenID-tunnistetta ei löydy" @@ -307,6 +405,7 @@ finnishMessage (ConfirmationEmailSent email) = "Vahvistussähköposti on lähetty osoitteeseen " `mappend` email `mappend` "." + finnishMessage AddressVerified = "Sähköpostiosoite vahvistettu. Anna uusi salasana" finnishMessage InvalidKeyTitle = "Virheellinen varmistusavain" finnishMessage InvalidKey = "Valitettavasti varmistusavain on virheellinen." @@ -325,5 +424,53 @@ finnishMessage NowLoggedIn = "Olet nyt kirjautunut sisään" finnishMessage LoginTitle = "Kirjautuminen" finnishMessage PleaseProvideUsername = "Käyttäjänimi puuttuu" finnishMessage PleaseProvidePassword = "Salasana puuttuu" +finnishMessage NoIdentifierProvided = "Sähköpostiosoite/käyttäjänimi puuttuu" +finnishMessage InvalidEmailAddress = "Annettu sähköpostiosoite ei kelpaa" +finnishMessage PasswordResetTitle = "Uuden salasanan tilaaminen" +finnishMessage ProvideIdentifier = "Sähköpostiosoite tai käyttäjänimi" +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 InvalidUsernamePass = "Virheellinen käyttäjänimi tai salasana." + +chineseMessage :: AuthMessage -> Text +chineseMessage NoOpenID = "无效的OpenID" +chineseMessage LoginOpenID = "用OpenID登录" +chineseMessage LoginGoogle = "用Google帐户登录" +chineseMessage LoginYahoo = "用Yahoo帐户登录" +chineseMessage Email = "邮箱" +chineseMessage Password = "密码" +chineseMessage Register = "注册" +chineseMessage RegisterLong = "注册新帐户" +chineseMessage EnterEmail = "输入你的邮箱地址,你将收到一封确认邮件。" +chineseMessage ConfirmationEmailSentTitle = "确认邮件已发送" +chineseMessage (ConfirmationEmailSent email) = + "确认邮件已发送至 " `mappend` + email `mappend` + "." +chineseMessage AddressVerified = "地址验证成功,请设置新密码" +chineseMessage InvalidKeyTitle = "无效的验证码" +chineseMessage InvalidKey = "对不起,验证码无效。" +chineseMessage InvalidEmailPass = "无效的邮箱/密码组合" +chineseMessage BadSetPass = "你需要登录才能设置密码" +chineseMessage SetPassTitle = "设置密码" +chineseMessage SetPass = "设置新密码" +chineseMessage NewPass = "新密码" +chineseMessage ConfirmPass = "确认" +chineseMessage PassMismatch = "密码不匹配,请重新输入" +chineseMessage PassUpdated = "密码更新成功" +chineseMessage Facebook = "用Facebook帐户登录" +chineseMessage LoginViaEmail = "用邮箱登录" +chineseMessage InvalidLogin = "登录失败" +chineseMessage NowLoggedIn = "登录成功" +chineseMessage LoginTitle = "登录" +chineseMessage PleaseProvideUsername = "请输入用户名" +chineseMessage PleaseProvidePassword = "请输入密码" +chineseMessage NoIdentifierProvided = "缺少邮箱/用户名" +chineseMessage InvalidEmailAddress = "无效的邮箱地址" +chineseMessage PasswordResetTitle = "重置密码" +chineseMessage ProvideIdentifier = "邮箱或用户名" +chineseMessage SendPasswordResetEmail = "发送密码重置邮件" +chineseMessage PasswordResetPrompt = "输入你的邮箱地址或用户名,你将收到一封密码重置邮件。" +chineseMessage InvalidUsernamePass = "无效的用户名/密码组合" diff --git a/yesod-auth/Yesod/Auth/OpenId.hs b/yesod-auth/Yesod/Auth/OpenId.hs index 5e216bf6..d2e20c5b 100644 --- a/yesod-auth/Yesod/Auth/OpenId.hs +++ b/yesod-auth/Yesod/Auth/OpenId.hs @@ -1,6 +1,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE RankNTypes #-} module Yesod.Auth.OpenId ( authOpenId , forwardUrl @@ -14,15 +15,8 @@ import Yesod.Auth import qualified Web.Authenticate.OpenId as OpenId import Yesod.Form -import Yesod.Handler -import Yesod.Widget (toWidget, whamlet) -import Yesod.Request +import Yesod.Core import Text.Cassius (cassius) -#if MIN_VERSION_blaze_html(0, 5, 0) -import Text.Blaze.Html (toHtml) -#else -import Text.Blaze (toHtml) -#endif import Data.Text (Text, isPrefixOf) import qualified Yesod.Auth.Message as Msg import Control.Exception.Lifted (SomeException, try) @@ -43,7 +37,7 @@ authOpenId idType extensionFields = complete = PluginR "openid" ["complete"] name = "openid_identifier" login tm = do - ident <- lift newIdent + ident <- newIdent -- FIXME this is a hack to get GHC 7.6's type checker to allow the -- code, but it shouldn't be necessary let y :: a -> [(Text, Text)] -> Text @@ -66,23 +60,21 @@ $newline never |] dispatch "GET" ["forward"] = do - roid <- runInputGet $ iopt textField name + roid <- lift $ runInputGet $ iopt textField name case roid of Just oid -> do render <- getUrlRender - toMaster <- getRouteToMaster - let complete' = render $ toMaster complete - master <- getYesod + let complete' = render complete + master <- lift getYesod eres <- lift $ try $ OpenId.getForwardUrl oid complete' Nothing extensionFields (authHttpManager master) case eres of Left err -> do setMessage $ toHtml $ show (err :: SomeException) - redirect $ toMaster LoginR + redirect LoginR Right x -> redirect x Nothing -> do - toMaster <- getRouteToMaster - setMessageI Msg.NoOpenID - redirect $ toMaster LoginR + lift $ setMessageI Msg.NoOpenID + redirect LoginR dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues dispatch "GET" ["complete"] = do rr <- getRequest @@ -93,14 +85,13 @@ $newline never completeHelper idType posts dispatch _ _ = notFound -completeHelper :: YesodAuth m => IdentifierType -> [(Text, Text)] -> GHandler Auth m () +completeHelper :: IdentifierType -> [(Text, Text)] -> AuthHandler master () completeHelper idType gets' = do - master <- getYesod - eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master) - toMaster <- getRouteToMaster + master <- lift getYesod + eres <- try $ OpenId.authenticateClaimed gets' (authHttpManager master) let onFailure err = do setMessage $ toHtml $ show (err :: SomeException) - redirect $ toMaster LoginR + redirect LoginR let onSuccess oir = do let claimed = case OpenId.oirClaimed oir of @@ -114,7 +105,7 @@ completeHelper idType gets' = do case idType of OPLocal -> OpenId.oirOpLocal oir Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir - setCreds True $ Creds "openid" i gets'' + lift $ setCreds True $ Creds "openid" i gets'' either onFailure onSuccess eres -- | The main identifier provided by the OpenID authentication plugin is the diff --git a/yesod-auth/Yesod/Auth/Routes.hs b/yesod-auth/Yesod/Auth/Routes.hs new file mode 100644 index 00000000..cb182d58 --- /dev/null +++ b/yesod-auth/Yesod/Auth/Routes.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveDataTypeable #-} +module Yesod.Auth.Routes where + +import Yesod.Core +import Data.Text (Text) + +data Auth = Auth + +mkYesodSubData "Auth" [parseRoutes| +/check CheckR GET +/login LoginR GET +/logout LogoutR GET POST +/page/#Text/*Texts PluginR +|] diff --git a/yesod-auth/Yesod/Auth/Rpxnow.hs b/yesod-auth/Yesod/Auth/Rpxnow.hs index 0c803004..dc674e68 100644 --- a/yesod-auth/Yesod/Auth/Rpxnow.hs +++ b/yesod-auth/Yesod/Auth/Rpxnow.hs @@ -10,9 +10,7 @@ import Yesod.Auth import qualified Web.Authenticate.Rpxnow as Rpxnow import Control.Monad (mplus) -import Yesod.Handler -import Yesod.Widget -import Yesod.Request +import Yesod.Core import Text.Hamlet (hamlet) import Data.Text (pack, unpack) import Data.Text.Encoding (encodeUtf8, decodeUtf8With) @@ -27,12 +25,8 @@ authRpxnow :: YesodAuth m authRpxnow app apiKey = AuthPlugin "rpxnow" dispatch login where - login :: - forall sub master. - ToWidget sub master (GWidget sub master ()) - => (Route Auth -> Route master) -> GWidget sub master () login tm = do - render <- lift getUrlRender + render <- getUrlRender let queryString = decodeUtf8With lenientDecode $ renderQuery True [("token_url", Just $ encodeUtf8 $ render $ tm $ PluginR "rpxnow" [])] toWidget [hamlet| @@ -45,7 +39,7 @@ $newline never token <- case token1 ++ token2 of [] -> invalidArgs ["token: Value not supplied"] x:_ -> return $ unpack x - master <- getYesod + master <- lift getYesod Rpxnow.Identifier ident extra <- lift $ Rpxnow.authenticate apiKey token (authHttpManager master) let creds = Creds "rpxnow" ident @@ -54,7 +48,7 @@ $newline never $ maybe id (\x -> (:) ("displayName", x)) (fmap pack $ getDisplayName $ map (unpack *** unpack) extra) [] - setCreds True creds + lift $ setCreds True creds dispatch _ _ = notFound -- | Get some form of a display name. diff --git a/yesod-auth/yesod-auth.cabal b/yesod-auth/yesod-auth.cabal index 4bebd901..59baf769 100644 --- a/yesod-auth/yesod-auth.cabal +++ b/yesod-auth/yesod-auth.cabal @@ -1,5 +1,5 @@ name: yesod-auth -version: 1.1.7 +version: 1.2.0 license: MIT license-file: LICENSE author: Michael Snoyman, Patrick Brisbin @@ -17,24 +17,23 @@ library build-depends: base >= 4 && < 5 , authenticate >= 1.3 , bytestring >= 0.9.1.4 - , yesod-core >= 1.1 && < 1.2 - , wai >= 1.3 + , yesod-core >= 1.2 && < 1.3 + , wai >= 1.4 , template-haskell , pureMD5 >= 2.0 , random >= 1.0.0.2 , text >= 0.7 , mime-mail >= 0.3 - , yesod-persistent >= 1.1 + , yesod-persistent >= 1.2 , hamlet >= 1.1 && < 1.2 , shakespeare-css >= 1.0 && < 1.1 , shakespeare-js >= 1.0.2 && < 1.2 - , yesod-json >= 1.1 && < 1.2 , containers , unordered-containers - , yesod-form >= 1.1 && < 1.3 + , yesod-form >= 1.3 && < 1.4 , transformers >= 0.2.2 - , persistent >= 1.0 && < 1.2 - , persistent-template >= 1.0 && < 1.2 + , persistent >= 1.2 && < 1.3 + , persistent-template >= 1.2 && < 1.3 , SHA >= 1.4.1.3 , http-conduit >= 1.5 , aeson >= 0.5 @@ -45,6 +44,8 @@ library , network , http-types , file-embed + , email-validate >= 1.0 + , data-default exposed-modules: Yesod.Auth Yesod.Auth.BrowserId @@ -55,6 +56,7 @@ library Yesod.Auth.HashDB Yesod.Auth.Message Yesod.Auth.GoogleEmail + other-modules: Yesod.Auth.Routes ghc-options: -Wall source-repository head diff --git a/yesod/AddHandler.hs b/yesod-bin/AddHandler.hs similarity index 99% rename from yesod/AddHandler.hs rename to yesod-bin/AddHandler.hs index 04ffc8cf..49fe5685 100644 --- a/yesod/AddHandler.hs +++ b/yesod-bin/AddHandler.hs @@ -89,7 +89,7 @@ mkHandler name pattern methods = unlines where go method = [ "" - , concat $ func : " :: " : map toArrow types ++ ["Handler RepHtml"] + , concat $ func : " :: " : map toArrow types ++ ["Handler Html"] , concat [ func , " = error \"Not yet implemented: " diff --git a/yesod/Build.hs b/yesod-bin/Build.hs similarity index 100% rename from yesod/Build.hs rename to yesod-bin/Build.hs diff --git a/yesod/Devel.hs b/yesod-bin/Devel.hs similarity index 98% rename from yesod/Devel.hs rename to yesod-bin/Devel.hs index 7958ac7e..378d9575 100644 --- a/yesod/Devel.hs +++ b/yesod-bin/Devel.hs @@ -123,7 +123,10 @@ reverseProxy opts iappPort = do return $ Right $ ProxyDest "127.0.0.1" appPort) def { wpsOnExc = onExc - , wpsTimeout = Just (1000000 * proxyTimeout opts) + , wpsTimeout = + if proxyTimeout opts == 0 + then Nothing + else Just (1000000 * proxyTimeout opts) } manager putStrLn "Reverse proxy stopped, but it shouldn't" diff --git a/yesod/GhcBuild.hs b/yesod-bin/GhcBuild.hs similarity index 100% rename from yesod/GhcBuild.hs rename to yesod-bin/GhcBuild.hs diff --git a/yesod/Keter.hs b/yesod-bin/Keter.hs similarity index 100% rename from yesod/Keter.hs rename to yesod-bin/Keter.hs diff --git a/yesod-default/LICENSE b/yesod-bin/LICENSE similarity index 100% rename from yesod-default/LICENSE rename to yesod-bin/LICENSE diff --git a/yesod/Options.hs b/yesod-bin/Options.hs similarity index 95% rename from yesod/Options.hs rename to yesod-bin/Options.hs index d0b94765..25b3d940 100644 --- a/yesod/Options.hs +++ b/yesod-bin/Options.hs @@ -70,13 +70,8 @@ injectDefaultP env path p@(OptP o) let (Just parseri) = f cmd in parseri { infoParser = injectDefaultP env (path ++ [normalizeName cmd]) (infoParser parseri) } in OptP (Option (CmdReader cmds (`M.lookup` cmdMap)) props) -#if MIN_VERSION_optparse_applicative(0, 5, 0) | (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) -#else - | (Option (OptReader names (CReader _ rdr)) _) <- o = - p <|> maybe empty pure (msum $ map (rdr <=< getEnvValue env path) names) -#endif | (Option (FlagReader names a) _) <- o = p <|> if any ((==Just "1") . getEnvValue env path) names then pure a else empty | otherwise = p diff --git a/yesod/Scaffolding/Scaffolder.hs b/yesod-bin/Scaffolding/Scaffolder.hs similarity index 100% rename from yesod/Scaffolding/Scaffolder.hs rename to yesod-bin/Scaffolding/Scaffolder.hs diff --git a/yesod-default/Setup.lhs b/yesod-bin/Setup.lhs similarity index 100% rename from yesod-default/Setup.lhs rename to yesod-bin/Setup.lhs diff --git a/yesod/ghcwrapper.hs b/yesod-bin/ghcwrapper.hs similarity index 100% rename from yesod/ghcwrapper.hs rename to yesod-bin/ghcwrapper.hs diff --git a/yesod/hsfiles/mongo.hsfiles b/yesod-bin/hsfiles/mongo.hsfiles similarity index 100% rename from yesod/hsfiles/mongo.hsfiles rename to yesod-bin/hsfiles/mongo.hsfiles diff --git a/yesod/hsfiles/mysql.hsfiles b/yesod-bin/hsfiles/mysql.hsfiles similarity index 100% rename from yesod/hsfiles/mysql.hsfiles rename to yesod-bin/hsfiles/mysql.hsfiles diff --git a/yesod/hsfiles/postgres-fay.hsfiles b/yesod-bin/hsfiles/postgres-fay.hsfiles similarity index 100% rename from yesod/hsfiles/postgres-fay.hsfiles rename to yesod-bin/hsfiles/postgres-fay.hsfiles diff --git a/yesod/hsfiles/postgres.hsfiles b/yesod-bin/hsfiles/postgres.hsfiles similarity index 100% rename from yesod/hsfiles/postgres.hsfiles rename to yesod-bin/hsfiles/postgres.hsfiles diff --git a/yesod/hsfiles/simple.hsfiles b/yesod-bin/hsfiles/simple.hsfiles similarity index 100% rename from yesod/hsfiles/simple.hsfiles rename to yesod-bin/hsfiles/simple.hsfiles diff --git a/yesod/hsfiles/sqlite.hsfiles b/yesod-bin/hsfiles/sqlite.hsfiles similarity index 100% rename from yesod/hsfiles/sqlite.hsfiles rename to yesod-bin/hsfiles/sqlite.hsfiles diff --git a/yesod/input/database.cg b/yesod-bin/input/database.cg similarity index 100% rename from yesod/input/database.cg rename to yesod-bin/input/database.cg diff --git a/yesod/input/done.cg b/yesod-bin/input/done.cg similarity index 100% rename from yesod/input/done.cg rename to yesod-bin/input/done.cg diff --git a/yesod/input/welcome.cg b/yesod-bin/input/welcome.cg similarity index 100% rename from yesod/input/welcome.cg rename to yesod-bin/input/welcome.cg diff --git a/yesod/main.hs b/yesod-bin/main.hs similarity index 94% rename from yesod/main.hs rename to yesod-bin/main.hs index 9a8a482f..df72e2a1 100755 --- a/yesod/main.hs +++ b/yesod-bin/main.hs @@ -8,18 +8,14 @@ import Options.Applicative import System.Exit (ExitCode (ExitSuccess), exitWith) import System.Process (rawSystem) -import Yesod.Core (yesodVersion) - import AddHandler (addHandler) import Devel (DevelOpts (..), devel) import Keter (keter) import Options (injectDefaults) -import qualified Paths_yesod +import qualified Paths_yesod_bin import Scaffolding.Scaffolder -#if MIN_VERSION_optparse_applicative(0, 5, 0) import Options.Applicative.Builder.Internal (Mod, OptionFields) -#endif #ifndef WINDOWS import Build (touch) @@ -98,8 +94,7 @@ main = do Touch -> touch' Devel da s f r b _ig es p t -> devel (DevelOpts (optCabalPgm o == CabalDev) da (optVerbose o) r s f b p t) es Keter noRebuild -> keter (cabalCommand o) noRebuild - Version -> do putStrLn ("yesod-core version:" ++ yesodVersion) - putStrLn ("yesod version:" ++ showVersion Paths_yesod.version) + Version -> do putStrLn ("yesod-bin version: " ++ showVersion Paths_yesod_bin.version) AddHandler -> addHandler Test -> do touch' cabal ["configure", "--enable-tests", "-flibrary-only"] @@ -153,8 +148,8 @@ develOptions = Devel <$> switch ( long "disable-api" <> short 'd' <*> extraCabalArgs <*> option ( long "port" <> short 'p' <> value 3000 <> metavar "N" <> help "Devel server listening port" ) - <*> option ( long "proxy-timeout" <> short 'x' <> value 10 <> metavar "N" - <> help "Devel server timeout before returning 'not ready' message (in seconds)" ) + <*> option ( long "proxy-timeout" <> short 'x' <> value 0 <> metavar "N" + <> help "Devel server timeout before returning 'not ready' message (in seconds, 0 for none)" ) extraCabalArgs :: Parser [String] extraCabalArgs = many (strOption ( long "extra-cabal-arg" <> short 'e' <> metavar "ARG" @@ -166,11 +161,7 @@ optStr :: Mod OptionFields (Maybe String) -> Parser (Maybe String) optStr m = nullOption $ value Nothing <> reader (success . str) <> m where -#if MIN_VERSION_optparse_applicative(0, 5, 0) success = Right -#else - success = Just -#endif -- | Like @rawSystem@, but exits if it receives a non-success result. rawSystem' :: String -> [String] -> IO () diff --git a/yesod/update-hsfiles.sh b/yesod-bin/update-hsfiles.sh similarity index 100% rename from yesod/update-hsfiles.sh rename to yesod-bin/update-hsfiles.sh diff --git a/yesod-bin/yesod-bin.cabal b/yesod-bin/yesod-bin.cabal new file mode 100644 index 00000000..b4257db9 --- /dev/null +++ b/yesod-bin/yesod-bin.cabal @@ -0,0 +1,103 @@ +name: yesod-bin +version: 1.2.0 +license: MIT +license-file: LICENSE +author: Michael Snoyman +maintainer: Michael Snoyman +synopsis: The yesod helper executable. +description: Provides scaffolding, devel server, and some simple code generation helpers. +category: Web, Yesod +stability: Stable +cabal-version: >= 1.6 +build-type: Simple +homepage: http://www.yesodweb.com/ + +extra-source-files: + input/*.cg + hsfiles/mongo.hsfiles + hsfiles/mysql.hsfiles + hsfiles/postgres.hsfiles + hsfiles/postgres-fay.hsfiles + hsfiles/simple.hsfiles + hsfiles/sqlite.hsfiles + +executable yesod-ghc-wrapper + main-is: ghcwrapper.hs + build-depends: + base >= 4 && < 5 + , Cabal + +executable yesod-ld-wrapper + main-is: ghcwrapper.hs + cpp-options: -DLDCMD + build-depends: + base >= 4 && < 5 + , Cabal +executable yesod-ar-wrapper + main-is: ghcwrapper.hs + cpp-options: -DARCMD + build-depends: + base >= 4 && < 5 + , Cabal + +executable yesod + if os(windows) + cpp-options: -DWINDOWS + build-depends: base >= 4.3 && < 5 + , ghc >= 7.0.3 && < 7.8 + , ghc-paths >= 0.1 + , parsec >= 2.1 && < 4 + , text >= 0.11 + , shakespeare-text >= 1.0 && < 1.1 + , shakespeare >= 1.0.2 && < 1.1 + , shakespeare-js >= 1.0.2 && < 1.2 + , shakespeare-css >= 1.0.2 && < 1.1 + , bytestring >= 0.9.1.4 + , time >= 1.1.4 + , template-haskell + , directory >= 1.0 + , Cabal + , unix-compat >= 0.2 && < 0.5 + , containers >= 0.2 + , attoparsec >= 0.10 + , http-types >= 0.7 + , blaze-builder >= 0.2.1.4 && < 0.4 + , filepath >= 1.1 + , process + , zlib >= 0.5 && < 0.6 + , tar >= 0.4 && < 0.5 + , system-filepath >= 0.4 && < 0.5 + , system-fileio >= 0.3 && < 0.4 + , unordered-containers + , yaml >= 0.8 && < 0.9 + , optparse-applicative >= 0.5 + , fsnotify >= 0.0 && < 0.1 + , split >= 0.2 && < 0.3 + , file-embed + , conduit >= 0.5 && < 1.1 + , resourcet >= 0.3 && < 0.5 + , base64-bytestring + , lifted-base + , http-reverse-proxy >= 0.1.1 + , network + , http-conduit + , network-conduit + , project-template >= 0.1.1 + , transformers + , warp >= 1.3.7.5 + , wai >= 1.4 + + ghc-options: -Wall -threaded + main-is: main.hs + other-modules: Scaffolding.Scaffolder + Devel + Build + GhcBuild + Keter + AddHandler + Paths_yesod_bin + Options + +source-repository head + type: git + location: https://github.com/yesodweb/yesod diff --git a/yesod-core/Yesod/Content.hs b/yesod-core/Yesod/Content.hs deleted file mode 100644 index 0ececbb8..00000000 --- a/yesod-core/Yesod/Content.hs +++ /dev/null @@ -1,263 +0,0 @@ -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE TypeSynonymInstances #-} -{-# LANGUAGE Rank2Types #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE CPP #-} -module Yesod.Content - ( -- * Content - Content (..) - , emptyContent - , ToContent (..) - -- * Mime types - -- ** Data type - , ContentType - , typeHtml - , typePlain - , typeJson - , typeXml - , typeAtom - , typeRss - , typeJpeg - , typePng - , typeGif - , typeSvg - , typeJavascript - , typeCss - , typeFlv - , typeOgv - , typeOctet - -- * Utilities - , simpleContentType - -- * Evaluation strategy - , DontFullyEvaluate (..) - -- * Representations - , ChooseRep - , HasReps (..) - , defChooseRep - -- ** Specific content types - , RepHtml (..) - , RepJson (..) - , RepHtmlJson (..) - , RepPlain (..) - , RepXml (..) - -- * Utilities - , formatW3 - , formatRFC1123 - , formatRFC822 - ) where - -import Data.Maybe (mapMaybe) -import qualified Data.ByteString as B -import qualified Data.ByteString.Lazy as L -import Data.Text.Lazy (Text, pack) -import qualified Data.Text as T - -import Data.Time -import System.Locale - -import qualified Data.Text.Encoding -import qualified Data.Text.Lazy.Encoding - -import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString) -import Data.Monoid (mempty) - -import Text.Hamlet (Html) -import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder) -import Data.String (IsString (fromString)) -import Network.Wai (FilePart) -import Data.Conduit (Source, ResourceT, Flush) - -import qualified Data.Aeson as J -import Data.Aeson.Encode (fromValue) -import qualified Blaze.ByteString.Builder.Char.Utf8 as Blaze -import Data.Text.Lazy.Builder (toLazyText) - -data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length. - | ContentSource !(Source (ResourceT IO) (Flush Builder)) - | ContentFile !FilePath !(Maybe FilePart) - | ContentDontEvaluate !Content - --- | Zero-length enumerator. -emptyContent :: Content -emptyContent = ContentBuilder mempty $ Just 0 - -instance IsString Content where - fromString = toContent - --- | Anything which can be converted into 'Content'. Most of the time, you will --- want to use the 'ContentBuilder' constructor. An easier approach will be to use --- a pre-defined 'toContent' function, such as converting your data into a lazy --- bytestring and then calling 'toContent' on that. --- --- Please note that the built-in instances for lazy data structures ('String', --- lazy 'L.ByteString', lazy 'Text' and 'Html') will not automatically include --- the content length for the 'ContentBuilder' constructor. -class ToContent a where - toContent :: a -> Content - -instance ToContent Builder where - toContent = flip ContentBuilder Nothing -instance ToContent B.ByteString where - toContent bs = ContentBuilder (fromByteString bs) $ Just $ B.length bs -instance ToContent L.ByteString where - toContent = flip ContentBuilder Nothing . fromLazyByteString -instance ToContent T.Text where - toContent = toContent . Data.Text.Encoding.encodeUtf8 -instance ToContent Text where - toContent = toContent . Data.Text.Lazy.Encoding.encodeUtf8 -instance ToContent String where - toContent = toContent . pack -instance ToContent Html where - toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing - --- | A function which gives targetted representations of content based on the --- content-types the user accepts. -type ChooseRep = - [ContentType] -- ^ list of content-types user accepts, ordered by preference - -> IO (ContentType, Content) - --- | Any type which can be converted to representations. -class HasReps a where - chooseRep :: a -> ChooseRep - --- | A helper method for generating 'HasReps' instances. --- --- This function should be given a list of pairs of content type and conversion --- functions. If none of the content types match, the first pair is used. -defChooseRep :: [(ContentType, a -> IO Content)] -> a -> ChooseRep -defChooseRep reps a ts = do - let (ct, c) = - case mapMaybe helper ts of - (x:_) -> x - [] -> case reps of - [] -> error "Empty reps to defChooseRep" - (x:_) -> x - c' <- c a - return (ct, c') - where - helper ct = do - c <- lookup ct reps - return (ct, c) - -instance HasReps ChooseRep where - chooseRep = id - -instance HasReps () where - chooseRep = defChooseRep [(typePlain, const $ return $ toContent B.empty)] - -instance HasReps (ContentType, Content) where - chooseRep = const . return - -instance HasReps [(ContentType, Content)] where - chooseRep a cts = return $ - case filter (\(ct, _) -> go ct `elem` map go cts) a of - ((ct, c):_) -> (ct, c) - _ -> case a of - (x:_) -> x - _ -> error "chooseRep [(ContentType, Content)] of empty" - where - go = simpleContentType - -newtype RepHtml = RepHtml Content -instance HasReps RepHtml where - chooseRep (RepHtml c) _ = return (typeHtml, c) -newtype RepJson = RepJson Content -instance HasReps RepJson where - chooseRep (RepJson c) _ = return (typeJson, c) -data RepHtmlJson = RepHtmlJson Content Content -instance HasReps RepHtmlJson where - chooseRep (RepHtmlJson html json) = chooseRep - [ (typeHtml, html) - , (typeJson, json) - ] -newtype RepPlain = RepPlain Content -instance HasReps RepPlain where - chooseRep (RepPlain c) _ = return (typePlain, c) -newtype RepXml = RepXml Content -instance HasReps RepXml where - chooseRep (RepXml c) _ = return (typeXml, c) - -type ContentType = B.ByteString -- FIXME Text? - -typeHtml :: ContentType -typeHtml = "text/html; charset=utf-8" - -typePlain :: ContentType -typePlain = "text/plain; charset=utf-8" - -typeJson :: ContentType -typeJson = "application/json; charset=utf-8" - -typeXml :: ContentType -typeXml = "text/xml" - -typeAtom :: ContentType -typeAtom = "application/atom+xml" - -typeRss :: ContentType -typeRss = "application/rss+xml" - -typeJpeg :: ContentType -typeJpeg = "image/jpeg" - -typePng :: ContentType -typePng = "image/png" - -typeGif :: ContentType -typeGif = "image/gif" - -typeSvg :: ContentType -typeSvg = "image/svg+xml" - -typeJavascript :: ContentType -typeJavascript = "text/javascript; charset=utf-8" - -typeCss :: ContentType -typeCss = "text/css; charset=utf-8" - -typeFlv :: ContentType -typeFlv = "video/x-flv" - -typeOgv :: ContentType -typeOgv = "video/ogg" - -typeOctet :: ContentType -typeOctet = "application/octet-stream" - --- | Removes \"extra\" information at the end of a content type string. In --- particular, removes everything after the semicolon, if present. --- --- For example, \"text/html; charset=utf-8\" is commonly used to specify the --- character encoding for HTML data. This function would return \"text/html\". -simpleContentType :: ContentType -> ContentType -simpleContentType = fst . B.breakByte 59 -- 59 == ; - --- | Format a 'UTCTime' in W3 format. -formatW3 :: UTCTime -> T.Text -formatW3 = T.pack . formatTime defaultTimeLocale "%FT%X-00:00" - --- | Format as per RFC 1123. -formatRFC1123 :: UTCTime -> T.Text -formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z" - --- | Format as per RFC 822. -formatRFC822 :: UTCTime -> T.Text -formatRFC822 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S %z" - --- | Prevents a response body from being fully evaluated before sending the --- request. --- --- Since 1.1.0 -newtype DontFullyEvaluate a = DontFullyEvaluate a - -instance HasReps a => HasReps (DontFullyEvaluate a) where - chooseRep (DontFullyEvaluate a) = fmap (fmap (fmap ContentDontEvaluate)) $ chooseRep a - -instance ToContent a => ToContent (DontFullyEvaluate a) where - toContent (DontFullyEvaluate a) = ContentDontEvaluate $ toContent a - -instance ToContent J.Value where - toContent = flip ContentBuilder Nothing - . Blaze.fromLazyText - . toLazyText - . fromValue diff --git a/yesod-core/Yesod/Core.hs b/yesod-core/Yesod/Core.hs index 7268d6cb..2c9879b9 100644 --- a/yesod-core/Yesod/Core.hs +++ b/yesod-core/Yesod/Core.hs @@ -1,16 +1,24 @@ {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -fno-warn-warnings-deprecations #-} module Yesod.Core ( -- * Type classes Yesod (..) , YesodDispatch (..) + , YesodSubDispatch (..) , RenderRoute (..) + , ParseRoute (..) + , RouteAttrs (..) -- ** Breadcrumbs , YesodBreadcrumbs (..) , breadcrumbs -- * Types , Approot (..) , FileUpload (..) + , ErrorResponse (..) -- * Utitlities , maybeAuthorized , widgetToPageContent @@ -35,41 +43,96 @@ module Yesod.Core , SessionBackend (..) , defaultClientSessionBackend , clientSessionBackend - , clientSessionBackend2 , clientSessionDateCacher , loadClientSession , Header(..) - , BackendSession -- * JS loaders - , loadJsYepnope , ScriptLoadPosition (..) , BottomOfHeadAsync + -- * Subsites + , MonadHandler (..) + , MonadWidget (..) + , getRouteToParent + , defaultLayoutSub -- * Misc , yesodVersion , yesodRender , runFakeHandler + -- * LiteApp + , module Yesod.Core.Internal.LiteApp + -- * Low-level + , yesodRunner -- * Re-exports - , module Yesod.Content - , module Yesod.Dispatch - , module Yesod.Handler - , module Yesod.Request - , module Yesod.Widget - , module Yesod.Message + , module Yesod.Core.Content + , module Yesod.Core.Dispatch + , module Yesod.Core.Handler + , module Yesod.Core.Widget + , module Yesod.Core.Json + , module Text.Shakespeare.I18N + , module Yesod.Core.Internal.Util + , module Text.Blaze.Html + , MonadTrans (..) + , MonadIO (..) + , MonadBase (..) + , MonadBaseControl + , MonadResource (..) + , MonadLogger ) where -import Yesod.Internal.Core -import Yesod.Internal (Header(..)) -import Yesod.Content -import Yesod.Dispatch -import Yesod.Handler -import Yesod.Request -import Yesod.Widget -import Yesod.Message +import Yesod.Core.Content +import Yesod.Core.Dispatch +import Yesod.Core.Handler +import Yesod.Core.Class.Handler +import Yesod.Core.Widget +import Yesod.Core.Json +import Yesod.Core.Types +import Text.Shakespeare.I18N +import Yesod.Core.Internal.Util (formatW3 , formatRFC1123 , formatRFC822) +import Text.Blaze.Html (Html, toHtml, preEscapedToMarkup) import Control.Monad.Logger +import Control.Monad.Trans.Class (MonadTrans (..)) +import Yesod.Core.Internal.Session +import Yesod.Core.Internal.Run (yesodRunner) +import Yesod.Core.Class.Yesod +import Yesod.Core.Class.Dispatch +import Yesod.Core.Class.Breadcrumbs +import Yesod.Core.Internal.Run (yesodRender, runFakeHandler) +import qualified Paths_yesod_core +import Data.Version (showVersion) +import Yesod.Routes.Class +import Control.Monad.IO.Class (MonadIO (..)) +import Control.Monad.Base (MonadBase (..)) +import Control.Monad.Trans.Control (MonadBaseControl (..)) + +import Control.Monad.Trans.Resource (MonadResource (..)) +import Yesod.Core.Internal.LiteApp -- | Return an 'Unauthorized' value, with the given i18n message. -unauthorizedI :: RenderMessage master msg => msg -> GHandler sub master AuthResult -unauthorizedI msg =do +unauthorizedI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => msg -> m AuthResult +unauthorizedI msg = do mr <- getMessageRender return $ Unauthorized $ mr msg + +yesodVersion :: String +yesodVersion = showVersion Paths_yesod_core.version + +-- | Return the same URL if the user is authorized to see it. +-- +-- Built on top of 'isAuthorized'. This is useful for building page that only +-- contain links to pages the user is allowed to see. +maybeAuthorized :: Yesod site + => Route site + -> Bool -- ^ is this a write request? + -> HandlerT site IO (Maybe (Route site)) +maybeAuthorized r isWrite = do + x <- isAuthorized r isWrite + return $ if x == Authorized then Just r else Nothing + +getRouteToParent :: Monad m => HandlerT child (HandlerT parent m) (Route child -> Route parent) +getRouteToParent = HandlerT $ return . handlerToParent + +defaultLayoutSub :: Yesod parent + => WidgetT child IO () + -> HandlerT child (HandlerT parent IO) Html +defaultLayoutSub cwidget = widgetToParentWidget cwidget >>= lift . defaultLayout diff --git a/yesod-core/Yesod/Core/Class/Breadcrumbs.hs b/yesod-core/Yesod/Core/Class/Breadcrumbs.hs new file mode 100644 index 00000000..84586055 --- /dev/null +++ b/yesod-core/Yesod/Core/Class/Breadcrumbs.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE OverloadedStrings #-} +module Yesod.Core.Class.Breadcrumbs where + +import Yesod.Core.Handler +import Yesod.Routes.Class +import Data.Text (Text) + +-- | A type-safe, concise method of creating breadcrumbs for pages. For each +-- resource, you declare the title of the page and the parent resource (if +-- present). +class YesodBreadcrumbs site where + -- | Returns the title and the parent resource, if available. If you return + -- a 'Nothing', then this is considered a top-level page. + breadcrumb :: Route site -> HandlerT site IO (Text , Maybe (Route site)) + +-- | Gets the title of the current page and the hierarchy of parent pages, +-- along with their respective titles. +breadcrumbs :: YesodBreadcrumbs site => HandlerT site IO (Text, [(Route site, Text)]) +breadcrumbs = do + x <- getCurrentRoute + case x of + Nothing -> return ("Not found", []) + Just y -> do + (title, next) <- breadcrumb y + z <- go [] next + return (title, z) + where + go back Nothing = return back + go back (Just this) = do + (title, next) <- breadcrumb this + go ((this, title) : back) next diff --git a/yesod-core/Yesod/Core/Class/Dispatch.hs b/yesod-core/Yesod/Core/Class/Dispatch.hs new file mode 100644 index 00000000..fe014ac2 --- /dev/null +++ b/yesod-core/Yesod/Core/Class/Dispatch.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE FlexibleContexts #-} +module Yesod.Core.Class.Dispatch where + +import Yesod.Routes.Class +import qualified Network.Wai as W +import Yesod.Core.Types +import Yesod.Core.Content +import Yesod.Core.Class.Yesod +import Yesod.Core.Class.Handler +import Yesod.Core.Internal.Run + +-- | This class is automatically instantiated when you use the template haskell +-- mkYesod function. You should never need to deal with it directly. +class Yesod site => YesodDispatch site where + yesodDispatch :: YesodRunnerEnv site -> W.Application + +class YesodSubDispatch sub m where + yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m + -> W.Application + +instance YesodSubDispatch WaiSubsite master where + yesodSubDispatch YesodSubRunnerEnv {..} req = + app req + where + WaiSubsite app = ysreGetSub $ yreSite $ ysreParentEnv + +-- | A helper function for creating YesodSubDispatch instances, used by the +-- internal generated code. +subHelper :: Monad m -- NOTE: This is incredibly similar in type signature to yesodRunner, should probably be pointed out/explained. + => HandlerT child (HandlerT parent m) TypedContent + -> YesodSubRunnerEnv child parent (HandlerT parent m) + -> Maybe (Route child) + -> W.Application +subHelper handlert YesodSubRunnerEnv {..} route = + ysreParentRunner base ysreParentEnv (fmap ysreToParentRoute route) + where + base = stripHandlerT (fmap toTypedContent handlert) ysreGetSub ysreToParentRoute route diff --git a/yesod-core/Yesod/Core/Class/Handler.hs b/yesod-core/Yesod/Core/Class/Handler.hs new file mode 100644 index 00000000..370956b8 --- /dev/null +++ b/yesod-core/Yesod/Core/Class/Handler.hs @@ -0,0 +1,92 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE UndecidableInstances #-} +module Yesod.Core.Class.Handler + ( MonadHandler (..) + , MonadWidget (..) + ) where + +import Yesod.Core.Types +import Data.Monoid (mempty) +import Control.Monad (liftM) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Resource (MonadResource, MonadResourceBase, ExceptionT (..)) +import Control.Monad.Trans.Class (lift) +import Data.Monoid (Monoid) +import Data.Conduit.Internal (Pipe, ConduitM) + +import Control.Monad.Trans.Identity ( IdentityT) +import Control.Monad.Trans.List ( ListT ) +import Control.Monad.Trans.Maybe ( MaybeT ) +import Control.Monad.Trans.Error ( ErrorT, Error) +import Control.Monad.Trans.Reader ( ReaderT ) +import Control.Monad.Trans.State ( StateT ) +import Control.Monad.Trans.Writer ( WriterT ) +import Control.Monad.Trans.RWS ( RWST ) +import qualified Control.Monad.Trans.RWS.Strict as Strict ( RWST ) +import qualified Control.Monad.Trans.State.Strict as Strict ( StateT ) +import qualified Control.Monad.Trans.Writer.Strict as Strict ( WriterT ) + +class MonadResource m => MonadHandler m where + type HandlerSite m + liftHandlerT :: HandlerT (HandlerSite m) IO a -> m a + +replaceToParent :: HandlerData site route -> HandlerData site () +replaceToParent hd = hd { handlerToParent = const () } + +instance MonadResourceBase m => MonadHandler (HandlerT site m) where + type HandlerSite (HandlerT site m) = site + liftHandlerT (HandlerT f) = HandlerT $ liftIO . f . replaceToParent +{-# RULES "liftHandlerT (HandlerT site IO)" forall action. liftHandlerT action = id #-} + +instance MonadResourceBase m => MonadHandler (WidgetT site m) where + type HandlerSite (WidgetT site m) = site + liftHandlerT (HandlerT f) = WidgetT $ liftIO . liftM (, mempty) . f . replaceToParent +{-# RULES "liftHandlerT (WidgetT site IO)" forall f. liftHandlerT (HandlerT f) = WidgetT $ liftM (, mempty) . f #-} + +#define GO(T) instance MonadHandler m => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; liftHandlerT = lift . liftHandlerT +#define GOX(X, T) instance (X, MonadHandler m) => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; liftHandlerT = lift . liftHandlerT +GO(IdentityT) +GO(ListT) +GO(MaybeT) +GOX(Error e, ErrorT e) +GO(ReaderT r) +GO(StateT s) +GOX(Monoid w, WriterT w) +GOX(Monoid w, RWST r w s) +GOX(Monoid w, Strict.RWST r w s) +GO(Strict.StateT s) +GOX(Monoid w, Strict.WriterT w) +GO(ExceptionT) +GO(Pipe l i o u) +GO(ConduitM i o) +#undef GO +#undef GOX + +class MonadHandler m => MonadWidget m where + liftWidgetT :: WidgetT (HandlerSite m) IO a -> m a +instance MonadResourceBase m => MonadWidget (WidgetT site m) where + liftWidgetT (WidgetT f) = WidgetT $ liftIO . f . replaceToParent + +#define GO(T) instance MonadWidget m => MonadWidget (T m) where liftWidgetT = lift . liftWidgetT +#define GOX(X, T) instance (X, MonadWidget m) => MonadWidget (T m) where liftWidgetT = lift . liftWidgetT +GO(IdentityT) +GO(ListT) +GO(MaybeT) +GOX(Error e, ErrorT e) +GO(ReaderT r) +GO(StateT s) +GOX(Monoid w, WriterT w) +GOX(Monoid w, RWST r w s) +GOX(Monoid w, Strict.RWST r w s) +GO(Strict.StateT s) +GOX(Monoid w, Strict.WriterT w) +GO(ExceptionT) +GO(Pipe l i o u) +GO(ConduitM i o) +#undef GO +#undef GOX diff --git a/yesod-core/Yesod/Core/Class/Yesod.hs b/yesod-core/Yesod/Core/Class/Yesod.hs new file mode 100644 index 00000000..ee964a23 --- /dev/null +++ b/yesod-core/Yesod/Core/Class/Yesod.hs @@ -0,0 +1,603 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +module Yesod.Core.Class.Yesod where + +import Control.Monad.Logger (logErrorS) +import Yesod.Core.Content +import Yesod.Core.Handler + +import Yesod.Routes.Class + +import Blaze.ByteString.Builder (Builder) +import Blaze.ByteString.Builder.Char.Utf8 (fromText) +import Control.Arrow ((***)) +import Control.Monad (forM, when, void) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther), + LogSource) +import qualified Data.ByteString.Char8 as S8 +import qualified Data.ByteString.Lazy as L +import Data.Aeson (object, (.=)) +import Data.List (foldl') +import Data.List (nub) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) +import Data.Monoid +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.Encoding.Error as TEE +import Data.Text.Lazy.Builder (toLazyText) +import Data.Text.Lazy.Encoding (encodeUtf8) +import Data.Word (Word64) +import Language.Haskell.TH.Syntax (Loc (..)) +import Network.HTTP.Types (encodePath) +import qualified Network.Wai as W +import Data.Default (def) +import Network.Wai.Parse (lbsBackEnd, + tempFileBackEnd) +import System.IO (stdout) +import System.Log.FastLogger (LogStr (..), Logger, + loggerDate, loggerPutStr, + mkLogger) +import System.Log.FastLogger.Date (ZonedDate) +import Text.Blaze (customAttribute, textTag, + toValue, (!)) +import Text.Blaze (preEscapedToMarkup) +import qualified Text.Blaze.Html5 as TBH +import Text.Hamlet +import Text.Julius +import qualified Web.ClientSession as CS +import Web.Cookie (parseCookies) +import Web.Cookie (SetCookie (..)) +import Yesod.Core.Types +import Yesod.Core.Internal.Session +import Yesod.Core.Widget +import Control.Monad.Trans.Class (lift) + +-- | Define settings for a Yesod applications. All methods have intelligent +-- defaults, and therefore no implementation is required. +class RenderRoute site => Yesod site where + -- | An absolute URL to the root of the application. Do not include + -- trailing slash. + -- + -- Default value: 'ApprootRelative'. This is valid under the following + -- conditions: + -- + -- * Your application is served from the root of the domain. + -- + -- * You do not use any features that require absolute URLs, such as Atom + -- feeds and XML sitemaps. + -- + -- If this is not true, you should override with a different + -- implementation. + approot :: Approot site + approot = ApprootRelative + + -- | Output error response pages. + -- + -- Default value: 'defaultErrorHandler'. + errorHandler :: ErrorResponse -> HandlerT site IO TypedContent + errorHandler = defaultErrorHandler + + -- | Applies some form of layout to the contents of a page. + defaultLayout :: WidgetT site IO () -> HandlerT site IO RepHtml + defaultLayout w = do + p <- widgetToPageContent w + mmsg <- getMessage + hamletToRepHtml [hamlet| + $newline never + $doctype 5 + + + #{pageTitle p} + ^{pageHead p} + + $maybe msg <- mmsg + #{msg} + ^{pageBody p} + |] + + -- | Override the rendering function for a particular URL. One use case for + -- this is to offload static hosting to a different domain name to avoid + -- sending cookies. + urlRenderOverride :: site -> Route site -> Maybe Builder + urlRenderOverride _ _ = Nothing + + -- | Determine if a request is authorized or not. + -- + -- Return 'Authorized' if the request is authorized, + -- 'Unauthorized' a message if unauthorized. + -- If authentication is required, return 'AuthenticationRequired'. + isAuthorized :: Route site + -> Bool -- ^ is this a write request? + -> HandlerT site IO AuthResult + isAuthorized _ _ = return Authorized + + -- | Determines whether the current request is a write request. By default, + -- this assumes you are following RESTful principles, and determines this + -- from request method. In particular, all except the following request + -- methods are considered write: GET HEAD OPTIONS TRACE. + -- + -- This function is used to determine if a request is authorized; see + -- 'isAuthorized'. + isWriteRequest :: Route site -> HandlerT site IO Bool + isWriteRequest _ = do + wai <- waiRequest + return $ W.requestMethod wai `notElem` + ["GET", "HEAD", "OPTIONS", "TRACE"] + + -- | The default route for authentication. + -- + -- Used in particular by 'isAuthorized', but library users can do whatever + -- they want with it. + authRoute :: site -> Maybe (Route site) + authRoute _ = Nothing + + -- | A function used to clean up path segments. It returns 'Right' with a + -- clean path or 'Left' with a new set of pieces the user should be + -- redirected to. The default implementation enforces: + -- + -- * No double slashes + -- + -- * There is no trailing slash. + -- + -- Note that versions of Yesod prior to 0.7 used a different set of rules + -- involing trailing slashes. + cleanPath :: site -> [Text] -> Either [Text] [Text] + cleanPath _ s = + if corrected == s + then Right $ map dropDash s + else Left corrected + where + corrected = filter (not . T.null) s + dropDash t + | T.all (== '-') t = T.drop 1 t + | otherwise = t + + -- | Builds an absolute URL by concatenating the application root with the + -- pieces of a path and a query string, if any. + -- Note that the pieces of the path have been previously cleaned up by 'cleanPath'. + joinPath :: site + -> T.Text -- ^ application root + -> [T.Text] -- ^ path pieces + -> [(T.Text, T.Text)] -- ^ query string + -> Builder + joinPath _ ar pieces' qs' = + fromText ar `mappend` encodePath pieces qs + where + pieces = if null pieces' then [""] else map addDash pieces' + qs = map (TE.encodeUtf8 *** go) qs' + go "" = Nothing + go x = Just $ TE.encodeUtf8 x + addDash t + | T.all (== '-') t = T.cons '-' t + | otherwise = t + + -- | This function is used to store some static content to be served as an + -- external file. The most common case of this is stashing CSS and + -- JavaScript content in an external file; the "Yesod.Widget" module uses + -- this feature. + -- + -- The return value is 'Nothing' if no storing was performed; this is the + -- default implementation. A 'Just' 'Left' gives the absolute URL of the + -- file, whereas a 'Just' 'Right' gives the type-safe URL. The former is + -- necessary when you are serving the content outside the context of a + -- Yesod application, such as via memcached. + addStaticContent :: Text -- ^ filename extension + -> Text -- ^ mime-type + -> L.ByteString -- ^ content + -> HandlerT site IO (Maybe (Either Text (Route site, [(Text, Text)]))) + addStaticContent _ _ _ = return Nothing + + -- | Maximum allowed length of the request body, in bytes. + -- + -- If @Nothing@, no maximum is applied. + -- + -- Default: 2 megabytes. + maximumContentLength :: site -> Maybe (Route site) -> Maybe Word64 + maximumContentLength _ _ = Just $ 2 * 1024 * 1024 -- 2 megabytes + + -- | Creates a @Logger@ to use for log messages. + -- + -- Note that a common technique (endorsed by the scaffolding) is to create + -- a @Logger@ value and place it in your foundation datatype, and have this + -- method return that already created value. That way, you can use that + -- same @Logger@ for printing messages during app initialization. + -- + -- Default: Sends to stdout and automatically flushes on each write. + makeLogger :: site -> IO Logger + makeLogger _ = mkLogger True stdout + + -- | Send a message to the @Logger@ provided by @getLogger@. + -- + -- Default implementation: checks if the message should be logged using + -- 'shouldLog' and, if so, formats using 'formatLogMessage'. + messageLoggerSource :: site + -> Logger + -> Loc -- ^ position in source code + -> LogSource + -> LogLevel + -> LogStr -- ^ message + -> IO () + messageLoggerSource a logger loc source level msg = + when (shouldLog a source level) $ + formatLogMessage (loggerDate logger) loc source level msg >>= loggerPutStr logger + + -- | Where to Load sripts from. We recommend the default value, + -- 'BottomOfBody'. Alternatively use the built in async yepnope loader: + -- + -- > BottomOfHeadAsync $ loadJsYepnope $ Right $ StaticR js_modernizr_js + -- + -- Or write your own async js loader. + jsLoader :: site -> ScriptLoadPosition site + jsLoader _ = BottomOfBody + + -- | Create a session backend. Returning `Nothing' disables sessions. + -- + -- Default: Uses clientsession with a 2 hour timeout. + makeSessionBackend :: site -> IO (Maybe SessionBackend) + makeSessionBackend _ = fmap Just $ defaultClientSessionBackend 120 CS.defaultKeyFile + + -- | How to store uploaded files. + -- + -- Default: When the request body is greater than 50kb, store in a temp + -- file. For chunked request bodies, store in a temp file. Otherwise, store + -- in memory. + fileUpload :: site -> W.RequestBodyLength -> FileUpload + fileUpload _ (W.KnownLength size) + | size <= 50000 = FileUploadMemory lbsBackEnd + fileUpload _ _ = FileUploadDisk tempFileBackEnd + + -- | Should we log the given log source/level combination. + -- + -- Default: Logs everything at or above 'logLevel' + shouldLog :: site -> LogSource -> LogLevel -> Bool + shouldLog _ _ level = level >= LevelInfo + + -- | A Yesod middleware, which will wrap every handler function. This + -- allows you to run code before and after a normal handler. + -- + -- Default: the 'defaultYesodMiddleware' function. + -- + -- Since: 1.1.6 + yesodMiddleware :: HandlerT site IO res -> HandlerT site IO res + yesodMiddleware = defaultYesodMiddleware + +-- | Default implementation of 'yesodMiddleware'. Adds the response header +-- \"Vary: Accept, Accept-Language\" and performs authorization checks. +-- +-- Since 1.2.0 +defaultYesodMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res +defaultYesodMiddleware handler = do + addHeader "Vary" "Accept, Accept-Language" + authorizationCheck + handler + +-- | Check if a given request is authorized via 'isAuthorized' and +-- 'isWriteRequest'. +-- +-- Since 1.2.0 +authorizationCheck :: Yesod site => HandlerT site IO () +authorizationCheck = do + getCurrentRoute >>= maybe (return ()) checkUrl + where + checkUrl url = do + isWrite <- isWriteRequest url + ar <- isAuthorized url isWrite + case ar of + Authorized -> return () + AuthenticationRequired -> do + master <- getYesod + case authRoute master of + Nothing -> void $ notAuthenticated + Just url' -> do + void $ selectRep $ do + provideRepType typeHtml $ do + setUltDestCurrent + void $ redirect url' + provideRepType typeJson $ + void $ notAuthenticated + Unauthorized s' -> permissionDenied s' + +-- | Convert a widget to a 'PageContent'. +widgetToPageContent :: (Eq (Route site), Yesod site) + => WidgetT site IO () + -> HandlerT site IO (PageContent (Route site)) +widgetToPageContent w = do + master <- getYesod + hd <- HandlerT return + ((), GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- lift $ unWidgetT w hd + let title = maybe mempty unTitle mTitle + scripts = runUniqueList scripts' + stylesheets = runUniqueList stylesheets' + + render <- getUrlRenderParams + let renderLoc x = + case x of + Nothing -> Nothing + Just (Left s) -> Just s + Just (Right (u, p)) -> Just $ render u p + css <- forM (Map.toList style) $ \(mmedia, content) -> do + let rendered = toLazyText $ content render + x <- addStaticContent "css" "text/css; charset=utf-8" + $ encodeUtf8 rendered + return (mmedia, + case x of + Nothing -> Left $ preEscapedToMarkup rendered + Just y -> Right $ either id (uncurry render) y) + jsLoc <- + case jscript of + Nothing -> return Nothing + Just s -> do + x <- addStaticContent "js" "text/javascript; charset=utf-8" + $ encodeUtf8 $ renderJavascriptUrl render s + return $ renderLoc x + + -- modernizr should be at the end of the http://www.modernizr.com/docs/#installing + -- the asynchronous loader means your page doesn't have to wait for all the js to load + let (mcomplete, asyncScripts) = asyncHelper render scripts jscript jsLoc + regularScriptLoad = [hamlet| + $newline never + $forall s <- scripts + ^{mkScriptTag s} + $maybe j <- jscript + $maybe s <- jsLoc + " res - it "link from head async" $ runner HA $ do - res <- request defaultRequest - assertBody "\n" res - it "link from bottom" $ runner B $ do res <- request defaultRequest assertBody "\n" res -runner :: (YesodDispatch master master, Yesod master) => master -> Session () -> IO () +runner :: YesodDispatch master => master -> Session () -> IO () runner app f = toWaiApp app >>= runSession f diff --git a/yesod-core/test/YesodCoreTest/JsLoaderSites/HeadAsync.hs b/yesod-core/test/YesodCoreTest/JsLoaderSites/HeadAsync.hs deleted file mode 100644 index a7b4dceb..00000000 --- a/yesod-core/test/YesodCoreTest/JsLoaderSites/HeadAsync.hs +++ /dev/null @@ -1,16 +0,0 @@ -{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE FlexibleInstances #-} -module YesodCoreTest.JsLoaderSites.HeadAsync (HA(..), Widget) where - -import Yesod.Core - -data HA = HA -mkYesod "HA" [parseRoutes| -/ HeadAsyncR GET -|] -instance Yesod HA where - jsLoader _ = BottomOfHeadAsync $ loadJsYepnope $ Left "yepnope.js" - -getHeadAsyncR :: Handler RepHtml -getHeadAsyncR = defaultLayout $ addScriptRemote "load.js" diff --git a/yesod-core/test/YesodCoreTest/Json.hs b/yesod-core/test/YesodCoreTest/Json.hs new file mode 100644 index 00000000..09050ac7 --- /dev/null +++ b/yesod-core/test/YesodCoreTest/Json.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-} +module YesodCoreTest.Json (specs, Widget) where + +import Yesod.Core +import Test.Hspec +import qualified Data.Map as Map +import Network.Wai.Test +import Data.Text (Text) +import Data.ByteString.Lazy (ByteString) + +data App = App + +mkYesod "App" [parseRoutes| +/ HomeR GET +/has-multiple-pieces/#Int/#Int MultiplePiecesR GET +|] + +instance Yesod App + +getHomeR :: Handler RepPlain +getHomeR = do + val <- parseJsonBody_ + case Map.lookup ("foo" :: Text) val of + Nothing -> invalidArgs ["foo not found"] + Just foo -> return $ RepPlain $ toContent (foo :: Text) + +getMultiplePiecesR :: Int -> Int -> Handler () +getMultiplePiecesR _ _ = return () + +test :: String + -> ByteString + -> (SResponse -> Session ()) + -> Spec +test name rbody f = it name $ do + app <- toWaiApp App + flip runSession app $ do + sres <- srequest SRequest + { simpleRequest = defaultRequest + , simpleRequestBody = rbody + } + f sres + +specs :: Spec +specs = describe "Yesod.Json" $ do + test "parses valid content" "{\"foo\":\"bar\"}" $ \sres -> do + assertStatus 200 sres + assertBody "bar" sres + test "400 for bad JSON" "{\"foo\":\"bar\"" $ \sres -> do + assertStatus 400 sres + test "400 for bad structure" "{\"foo2\":\"bar\"}" $ \sres -> do + assertStatus 400 sres + assertBodyContains "foo not found" sres diff --git a/yesod-core/test/YesodCoreTest/Links.hs b/yesod-core/test/YesodCoreTest/Links.hs index 188cfe97..6dbe9ca7 100644 --- a/yesod-core/test/YesodCoreTest/Links.hs +++ b/yesod-core/test/YesodCoreTest/Links.hs @@ -5,12 +5,11 @@ module YesodCoreTest.Links (linksTest, Widget) where import Test.Hspec -import Yesod.Core hiding (Request) +import Yesod.Core import Text.Hamlet import Network.Wai import Network.Wai.Test import Data.Text (Text) -import Control.Monad.IO.Class (liftIO) import Blaze.ByteString.Builder (toByteString) data Y = Y @@ -18,8 +17,23 @@ mkYesod "Y" [parseRoutes| / RootR GET /single/#Text TextR GET /multi/*Texts TextsR GET + +/route-test-1/+[Text] RT1 GET +/route-test-2/*Vector-String RT2 GET +/route-test-3/*Vector-(Maybe-Int) RT3 GET +/route-test-4/#(Foo-Int-Int) RT4 GET |] +data Vector a = Vector + deriving (Show, Read, Eq) + +instance PathMultiPiece (Vector a) + +data Foo x y = Foo + deriving (Show, Read, Eq) + +instance PathPiece (Foo x y) + instance Yesod Y getRootR :: Handler RepHtml @@ -31,6 +45,18 @@ getTextR foo = defaultLayout $ toWidget [hamlet|%#{foo}%|] getTextsR :: [Text] -> Handler RepHtml getTextsR foos = defaultLayout $ toWidget [hamlet|%#{show foos}%|] +getRT1 :: [Text] -> Handler () +getRT1 _ = return () + +getRT2 :: Vector String -> Handler () +getRT2 _ = return () + +getRT3 :: Vector (Maybe Int) -> Handler () +getRT3 _ = return () + +getRT4 :: Foo Int Int -> Handler () +getRT4 _ = return () + linksTest :: Spec linksTest = describe "Test.Links" $ do it "linkToHome" case_linkToHome diff --git a/yesod-core/test/YesodCoreTest/LiteApp.hs b/yesod-core/test/YesodCoreTest/LiteApp.hs new file mode 100644 index 00000000..60de5095 --- /dev/null +++ b/yesod-core/test/YesodCoreTest/LiteApp.hs @@ -0,0 +1,42 @@ +module YesodCoreTest.LiteApp (specs) where + +import Yesod.Core +import Test.Hspec +import Network.Wai.Test +import Network.Wai +import qualified Data.ByteString.Char8 as S8 +import qualified Data.Text as T +import qualified Data.ByteString.Lazy.Char8 as L8 + +iapp :: IO Application +iapp = toWaiApp $ liteApp $ do + onMethod (S8.pack "GET") (dispatchTo $ return "GetHomepage") + onMethod (S8.pack "POST") (dispatchTo $ return "PostHomepage") + onStatic (T.pack "string") (withDynamic (\t -> dispatchTo $ return (t :: T.Text))) + onStatic (T.pack "multi") (withDynamicMulti (\[_, y] -> dispatchTo $ return (y :: T.Text))) + +test :: String -- ^ method + -> [String] -- ^ path + -> (Either Int String) -- ^ status code or body + -> Spec +test method path expected = it (method ++ " " ++ show path) $ do + app <- iapp + flip runSession app $ do + sres <- request defaultRequest + { requestMethod = S8.pack method + , pathInfo = map T.pack path + } + case expected of + Left i -> assertStatus i sres + Right b -> assertBody (L8.pack b) sres + +specs :: Spec +specs = describe "LiteApp" $ do + test "GET" [] $ Right "GetHomepage" + test "POST" [] $ Right "PostHomepage" + -- test "PUT" [] $ Left 405 + test "GET" ["string", "foo"] $ Right "foo" + test "DELETE" ["string", "bar"] $ Right "bar" + test "GET" ["string!", "foo"] $ Left 404 + test "GET" ["multi", "foo", "bar"] $ Right "bar" + test "GET" ["multi", "foo", "bar", "baz"] $ Left 500 diff --git a/yesod-core/test/YesodCoreTest/Media.hs b/yesod-core/test/YesodCoreTest/Media.hs index 490b6bdb..1dc52dca 100644 --- a/yesod-core/test/YesodCoreTest/Media.hs +++ b/yesod-core/test/YesodCoreTest/Media.hs @@ -5,7 +5,7 @@ module YesodCoreTest.Media (mediaTest, Widget) where import Test.Hspec -import Yesod.Core hiding (Request) +import Yesod.Core import Network.Wai import Network.Wai.Test import Text.Lucius @@ -15,9 +15,8 @@ mkYesodDispatch "Y" resourcesY instance Yesod Y where addStaticContent _ _ content = do - tm <- getRouteToMaster route <- getCurrentRoute - case fmap tm route of + case route of Just StaticR -> return $ Just $ Left $ if content == "foo2{bar:baz}" then "screen.css" @@ -27,7 +26,7 @@ instance Yesod Y where getRootR :: Handler RepHtml getRootR = defaultLayout $ do toWidget [lucius|foo1{bar:baz}|] - addCassiusMedia "screen" [lucius|foo2{bar:baz}|] + toWidgetMedia "screen" [lucius|foo2{bar:baz}|] toWidget [lucius|foo3{bar:baz}|] getStaticR :: Handler RepHtml diff --git a/yesod-core/test/YesodCoreTest/MediaData.hs b/yesod-core/test/YesodCoreTest/MediaData.hs index 6a33fab3..9036d41f 100644 --- a/yesod-core/test/YesodCoreTest/MediaData.hs +++ b/yesod-core/test/YesodCoreTest/MediaData.hs @@ -8,5 +8,5 @@ import Yesod.Core data Y = Y mkYesodData "Y" [parseRoutes| / RootR GET -/static StaticR GET +/static StaticR !IGNORED GET !alsoIgnored |] diff --git a/yesod-core/test/YesodCoreTest/NoOverloadedStrings.hs b/yesod-core/test/YesodCoreTest/NoOverloadedStrings.hs index d68707cd..7e344f5e 100644 --- a/yesod-core/test/YesodCoreTest/NoOverloadedStrings.hs +++ b/yesod-core/test/YesodCoreTest/NoOverloadedStrings.hs @@ -3,22 +3,40 @@ module YesodCoreTest.NoOverloadedStrings (noOverloadedTest, Widget) where import Test.Hspec +import YesodCoreTest.NoOverloadedStringsSub -import Yesod.Core hiding (Request) +import Yesod.Core +import Network.Wai import Network.Wai.Test import Data.Monoid (mempty) - -data Subsite = Subsite +import qualified Data.Text as T +import qualified Data.ByteString.Lazy.Char8 as L8 getSubsite :: a -> Subsite -getSubsite = const Subsite +getSubsite _ = Subsite $(mkYesodSubDispatch resourcesSubsite) -mkYesodSub "Subsite" [] [parseRoutes| -/bar BarR GET -|] +getBarR :: Monad m => m T.Text +getBarR = return $ T.pack "BarR" -getBarR :: GHandler Subsite m () -getBarR = return () +getBazR :: Yesod master => HandlerT Subsite (HandlerT master IO) RepHtml +getBazR = lift $ defaultLayout [whamlet|Used Default Layout|] + +getBinR :: Yesod master => HandlerT Subsite (HandlerT master IO) RepHtml +getBinR = do + widget <- widgetToParentWidget [whamlet| + Used defaultLayoutT + Baz + |] + lift $ defaultLayout widget + +getOnePiecesR :: Monad m => Int -> m () +getOnePiecesR _ = return () + +getTwoPiecesR :: Monad m => Int -> Int -> m () +getTwoPiecesR _ _ = return () + +getThreePiecesR :: Monad m => Int -> Int -> Int -> m () +getThreePiecesR _ _ _ = return () data Y = Y mkYesod "Y" [parseRoutes| @@ -43,6 +61,33 @@ case_sanity = runner $ do res <- request defaultRequest assertBody mempty res +case_subsite :: IO () +case_subsite = runner $ do + res <- request defaultRequest + { pathInfo = map T.pack ["subsite", "bar"] + } + assertBody (L8.pack "BarR") res + assertStatus 200 res + +case_deflayout :: IO () +case_deflayout = runner $ do + res <- request defaultRequest + { pathInfo = map T.pack ["subsite", "baz"] + } + assertBodyContains (L8.pack "Used Default Layout") res + assertStatus 200 res + +case_deflayoutT :: IO () +case_deflayoutT = runner $ do + res <- request defaultRequest + { pathInfo = map T.pack ["subsite", "bin"] + } + assertBodyContains (L8.pack "Used defaultLayoutT") res + assertStatus 200 res + noOverloadedTest :: Spec noOverloadedTest = describe "Test.NoOverloadedStrings" $ do it "sanity" case_sanity + it "subsite" case_subsite + it "deflayout" case_deflayout + it "deflayoutT" case_deflayoutT diff --git a/yesod-core/test/YesodCoreTest/NoOverloadedStringsSub.hs b/yesod-core/test/YesodCoreTest/NoOverloadedStringsSub.hs new file mode 100644 index 00000000..aa5d7c0e --- /dev/null +++ b/yesod-core/test/YesodCoreTest/NoOverloadedStringsSub.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE RankNTypes #-} +module YesodCoreTest.NoOverloadedStringsSub where + +import Yesod.Core +import Network.Wai +import Yesod.Core.Types + +data Subsite = Subsite (forall master. Yesod master => YesodSubRunnerEnv Subsite master (HandlerT master IO) -> Application) + +mkYesodSubData "Subsite" [parseRoutes| +/bar BarR GET +/baz BazR GET +/bin BinR GET +/has-one-piece/#Int OnePiecesR GET +/has-two-pieces/#Int/#Int TwoPiecesR GET +/has-three-pieces/#Int/#Int/#Int ThreePiecesR GET +|] + +instance Yesod master => YesodSubDispatch Subsite (HandlerT master IO) where + yesodSubDispatch ysre = + f ysre + where + Subsite f = ysreGetSub ysre $ yreSite $ ysreParentEnv ysre diff --git a/yesod-core/test/YesodCoreTest/Redirect.hs b/yesod-core/test/YesodCoreTest/Redirect.hs index 63d28759..da6f9725 100644 --- a/yesod-core/test/YesodCoreTest/Redirect.hs +++ b/yesod-core/test/YesodCoreTest/Redirect.hs @@ -2,7 +2,7 @@ module YesodCoreTest.Redirect (specs, Widget) where import YesodCoreTest.YesodTest -import Yesod.Handler (redirectWith) +import Yesod.Core.Handler (redirectWith) import qualified Network.HTTP.Types as H data Y = Y diff --git a/yesod-core/test/YesodCoreTest/Reps.hs b/yesod-core/test/YesodCoreTest/Reps.hs new file mode 100644 index 00000000..c1576797 --- /dev/null +++ b/yesod-core/test/YesodCoreTest/Reps.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-} +module YesodCoreTest.Reps (specs, Widget) where + +import Yesod.Core +import Test.Hspec +import Network.Wai +import Network.Wai.Test +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Char8 as S8 +import Data.String (IsString) +import Data.Text (Text) +import Data.Maybe (fromJust) +import Data.Monoid (Endo (..)) +import qualified Control.Monad.Trans.Writer as Writer +import qualified Data.Set as Set + +data App = App + +mkYesod "App" [parseRoutes| +/ HomeR GET !home +/json JsonR GET +/parent/#Int ParentR: + /#Text/child ChildR !child +|] + +instance Yesod App + +specialHtml :: IsString a => a +specialHtml = "text/html; charset=special" + +getHomeR :: Handler TypedContent +getHomeR = selectRep $ do + rep typeHtml "HTML" + rep specialHtml "HTMLSPECIAL" + rep typeXml "XML" + rep typeJson "JSON" + +rep :: Monad m => ContentType -> Text -> Writer.Writer (Data.Monoid.Endo [ProvidedRep m]) () +rep ct t = provideRepType ct $ return (t :: Text) + +getJsonR :: Handler TypedContent +getJsonR = selectRep $ do + rep typeHtml "HTML" + provideRep $ return $ object ["message" .= ("Invalid Login" :: Text)] + +handleChildR :: Int -> Text -> Handler () +handleChildR _ _ = return () + +testRequest :: Int -- ^ http status code + -> Request + -> ByteString -- ^ expected body + -> Spec +testRequest status req expected = it (S8.unpack $ fromJust $ lookup "Accept" $ requestHeaders req) $ do + app <- toWaiApp App + flip runSession app $ do + sres <- request req + assertStatus status sres + assertBody expected sres + +test :: String -- ^ accept header + -> ByteString -- ^ expected body + -> Spec +test accept expected = + testRequest 200 (acceptRequest accept) expected + +acceptRequest :: String -> Request +acceptRequest accept = defaultRequest + { requestHeaders = [("Accept", S8.pack accept)] + } + +specs :: Spec +specs = do + describe "selectRep" $ do + test "application/json" "JSON" + test (S8.unpack typeJson) "JSON" + test "text/xml" "XML" + test (S8.unpack typeXml) "XML" + test "text/xml,application/json" "XML" + test "text/xml;q=0.9,application/json;q=1.0" "JSON" + test (S8.unpack typeHtml) "HTML" + test "text/html" "HTML" + test specialHtml "HTMLSPECIAL" + testRequest 200 (acceptRequest "application/json") { pathInfo = ["json"] } "{\"message\":\"Invalid Login\"}" + testRequest 406 (acceptRequest "text/foo") "no match found for accept header" + test "text/*" "HTML" + test "*/*" "HTML" + describe "routeAttrs" $ do + it "HomeR" $ routeAttrs HomeR `shouldBe` Set.singleton "home" + it "JsonR" $ routeAttrs JsonR `shouldBe` Set.empty + it "ChildR" $ routeAttrs (ParentR 5 $ ChildR "ignored") `shouldBe` Set.singleton "child" diff --git a/yesod-core/test/YesodCoreTest/RequestBodySize.hs b/yesod-core/test/YesodCoreTest/RequestBodySize.hs index 937b887a..514559ac 100644 --- a/yesod-core/test/YesodCoreTest/RequestBodySize.hs +++ b/yesod-core/test/YesodCoreTest/RequestBodySize.hs @@ -5,7 +5,7 @@ module YesodCoreTest.RequestBodySize (specs, Widget) where import Test.Hspec -import Yesod.Core hiding (Request) +import Yesod.Core import Network.Wai import Network.Wai.Test @@ -29,7 +29,7 @@ mkYesod "Y" [parseRoutes| |] instance Yesod Y where - maximumContentLength _ _ = 10 + maximumContentLength _ _ = Just 10 postPostR, postConsumeR, postPartialConsumeR, postUnusedR :: Handler RepPlain @@ -38,13 +38,11 @@ postPostR = do return $ RepPlain $ toContent $ T.concat val postConsumeR = do - req <- waiRequest - body <- lift $ requestBody req $$ consume + body <- rawRequestBody $$ consume return $ RepPlain $ toContent $ S.concat body postPartialConsumeR = do - req <- waiRequest - body <- lift $ requestBody req $$ isolate 5 =$ consume + body <- rawRequestBody $$ isolate 5 =$ consume return $ RepPlain $ toContent $ S.concat body postUnusedR = return $ RepPlain "" @@ -75,6 +73,10 @@ caseHelper name path body statusChunked statusNonChunked = describe name $ do then [("content-length", S8.pack $ show $ S.length body)] else [] , requestMethod = "POST" + , requestBodyLength = + if includeLength + then KnownLength $ fromIntegral $ S.length body + else ChunkedBody } $ L.fromChunks $ map S.singleton $ S.unpack body specs :: Spec diff --git a/yesod-core/test/YesodCoreTest/Streaming.hs b/yesod-core/test/YesodCoreTest/Streaming.hs new file mode 100644 index 00000000..1b2fde72 --- /dev/null +++ b/yesod-core/test/YesodCoreTest/Streaming.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE OverloadedStrings #-} +module YesodCoreTest.Streaming (specs) where + +import Yesod.Core +import Test.Hspec +import Network.Wai.Test +import Data.Text (Text) +import Data.ByteString (ByteString) + +app :: LiteApp +app = liteApp $ dispatchTo $ respondSource typeHtml $ do + sendChunk ("Hello " :: String) + sendChunk ("World" :: ByteString) + sendChunk ("!\n" :: Text) + sendChunkHtml "<&>" + +test :: String + -> (SResponse -> Session ()) + -> Spec +test name f = it name $ do + wapp <- toWaiApp app + flip runSession wapp $ do + sres <- request defaultRequest + f sres + +specs :: Spec +specs = describe "Streaming" $ do + test "works" $ \sres -> do + assertStatus 200 sres + assertBody "Hello World!\n<&>" sres diff --git a/yesod-core/test/YesodCoreTest/Widget.hs b/yesod-core/test/YesodCoreTest/Widget.hs index 9b14ee35..c05a3f27 100644 --- a/yesod-core/test/YesodCoreTest/Widget.hs +++ b/yesod-core/test/YesodCoreTest/Widget.hs @@ -5,7 +5,7 @@ module YesodCoreTest.Widget (widgetTest) where import Test.Hspec -import Yesod.Core hiding (Request) +import Yesod.Core import Text.Julius import Text.Lucius import Text.Hamlet @@ -61,18 +61,18 @@ getTowidgetR = defaultLayout $ do getWhamletR :: Handler RepHtml getWhamletR = defaultLayout [whamlet| -$newline never -Test -@{WhamletR} -_{Goodbye} -_{MsgAnother} -^{embed} -|] + $newline never + Test + @{WhamletR} + _{Goodbye} + _{MsgAnother} + ^{embed} + |] where embed = [whamlet| -$newline never -Embed -|] + $newline never + Embed + |] getAutoR :: Handler RepHtml getAutoR = defaultLayout [whamlet| diff --git a/yesod-core/test/YesodCoreTest/YesodTest.hs b/yesod-core/test/YesodCoreTest/YesodTest.hs index 4cea7409..f0b7b69b 100644 --- a/yesod-core/test/YesodCoreTest/YesodTest.hs +++ b/yesod-core/test/YesodCoreTest/YesodTest.hs @@ -9,10 +9,10 @@ module YesodCoreTest.YesodTest , module Test.Hspec ) where -import Yesod.Core hiding (Request) +import Yesod.Core import Network.Wai.Test import Network.Wai import Test.Hspec -yesod :: (YesodDispatch y y, Yesod y) => y -> Session a -> IO a +yesod :: YesodDispatch y => y -> Session a -> IO a yesod app f = toWaiApp app >>= runSession f diff --git a/yesod-core/yesod-core.cabal b/yesod-core/yesod-core.cabal index f8717d70..988ce01d 100644 --- a/yesod-core/yesod-core.cabal +++ b/yesod-core/yesod-core.cabal @@ -1,5 +1,5 @@ name: yesod-core -version: 1.1.8.3 +version: 1.2.0 license: MIT license-file: LICENSE author: Michael Snoyman @@ -22,9 +22,9 @@ extra-source-files: test/YesodCoreTest/ErrorHandling.hs test/YesodCoreTest/Exceptions.hs test/YesodCoreTest/InternalRequest.hs + test/YesodCoreTest/Json.hs test/YesodCoreTest/JsLoader.hs test/YesodCoreTest/JsLoaderSites/Bottom.hs - test/YesodCoreTest/JsLoaderSites/HeadAsync.hs test/YesodCoreTest/Links.hs test/YesodCoreTest/Media.hs test/YesodCoreTest/MediaData.hs @@ -34,23 +34,16 @@ extra-source-files: test/YesodCoreTest/WaiSubsite.hs test/YesodCoreTest/Widget.hs test/YesodCoreTest/YesodTest.hs + test/YesodCoreTest/Auth.hs + test/YesodCoreTest/LiteApp.hs test/en.msg test/test.hs -flag test - description: Build the executable to run unit tests - default: False - library - -- Work around a bug in cabal. Without this, wai-test doesn't get built and - -- we have a missing dependency during --enable-tests builds. - if flag(test) - build-depends: wai-test - build-depends: base >= 4.3 && < 5 , time >= 1.1.4 - , yesod-routes >= 1.1 && < 1.2 - , wai >= 1.3 && < 1.5 + , yesod-routes >= 1.2 && < 1.3 + , wai >= 1.4 && < 1.5 , wai-extra >= 1.3 && < 1.4 , bytestring >= 0.9.1.4 , text >= 0.7 && < 0.12 @@ -63,7 +56,7 @@ library , shakespeare-i18n >= 1.0 && < 1.1 , blaze-builder >= 0.2.1.4 && < 0.4 , transformers >= 0.2.2 && < 0.4 - , clientsession >= 0.8 + , clientsession >= 0.9 && < 0.10 , random >= 1.0.0.2 && < 1.1 , cereal >= 0.3 && < 0.4 , old-locale >= 1.0.0.2 && < 1.1 @@ -79,26 +72,36 @@ library , vector >= 0.9 && < 0.11 , aeson >= 0.5 , fast-logger >= 0.2 - , monad-logger >= 0.2.1 && < 0.4 + , monad-logger >= 0.3.1 && < 0.4 , conduit >= 0.5 - , resourcet >= 0.3 && < 0.5 + , resourcet >= 0.4.6 && < 0.5 , lifted-base >= 0.1 + , attoparsec-conduit , blaze-html >= 0.5 , blaze-markup >= 0.5.1 + , data-default + , safe + , warp >= 1.3.8 - exposed-modules: Yesod.Content - Yesod.Core - Yesod.Dispatch - Yesod.Handler - Yesod.Request - Yesod.Widget - Yesod.Message - Yesod.Internal.TestApi - other-modules: Yesod.Internal - Yesod.Internal.Cache - Yesod.Internal.Core - Yesod.Internal.Session - Yesod.Internal.Request + exposed-modules: Yesod.Core + Yesod.Core.Content + Yesod.Core.Dispatch + Yesod.Core.Handler + Yesod.Core.Json + Yesod.Core.Widget + Yesod.Core.Internal + Yesod.Core.Types + other-modules: Yesod.Core.Internal.Session + Yesod.Core.Internal.Request + Yesod.Core.Class.Handler + Yesod.Core.Internal.Util + Yesod.Core.Internal.Response + Yesod.Core.Internal.Run + Yesod.Core.Internal.TH + Yesod.Core.Internal.LiteApp + Yesod.Core.Class.Yesod + Yesod.Core.Class.Dispatch + Yesod.Core.Class.Breadcrumbs Paths_yesod_core ghc-options: -Wall @@ -110,7 +113,7 @@ test-suite tests cpp-options: -DTEST build-depends: base ,hspec >= 1.3 - ,wai-test + ,wai-test >= 1.3.0.5 ,wai ,yesod-core ,bytestring @@ -125,6 +128,9 @@ test-suite tests ,QuickCheck >= 2 && < 3 ,transformers , conduit + , containers + , lifted-base + , resourcet ghc-options: -Wall source-repository head diff --git a/yesod-default/README b/yesod-default/README deleted file mode 100644 index e69de29b..00000000 diff --git a/yesod-default/yesod-default.cabal b/yesod-default/yesod-default.cabal deleted file mode 100644 index 130c7ba4..00000000 --- a/yesod-default/yesod-default.cabal +++ /dev/null @@ -1,51 +0,0 @@ -name: yesod-default -version: 1.1.3.2 -license: MIT -license-file: LICENSE -author: Patrick Brisbin -maintainer: Patrick Brisbin -synopsis: Default config and main functions for your yesod application -category: Web, Yesod -stability: Stable -cabal-version: >= 1.6 -build-type: Simple -homepage: http://www.yesodweb.com/ -description: Convenient wrappers for your the configuration and - execution of your yesod application - -library - if os(windows) - cpp-options: -DWINDOWS - - build-depends: base >= 4 && < 5 - , yesod-core >= 1.1 && < 1.2 - , warp >= 1.3 && < 1.4 - , wai >= 1.3 && < 1.5 - , wai-extra >= 1.3 && < 1.4 - , bytestring >= 0.9.1.4 - , transformers >= 0.2.2 && < 0.4 - , text >= 0.9 - , directory >= 1.0 - , shakespeare-css >= 1.0 && < 1.1 - , shakespeare-js >= 1.0 && < 1.2 - , template-haskell - , yaml >= 0.8 && < 0.9 - , network-conduit >= 0.5 - , unordered-containers - , hamlet >= 1.1 && < 1.2 - , data-default - , safe - - if !os(windows) - build-depends: unix - - exposed-modules: Yesod.Default.Config - , Yesod.Default.Main - , Yesod.Default.Util - , Yesod.Default.Handlers - - ghc-options: -Wall - -source-repository head - type: git - location: https://github.com/yesodweb/yesod diff --git a/yesod-examples/.gitignore b/yesod-examples/.gitignore deleted file mode 100644 index 1cd91990..00000000 --- a/yesod-examples/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -client_session_key.aes -dist -cabal-dev/ diff --git a/yesod-examples/LICENSE b/yesod-examples/LICENSE deleted file mode 100644 index d9f04179..00000000 --- a/yesod-examples/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2012 Michael Snoyman, http://www.yesodweb.com/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/yesod-examples/README b/yesod-examples/README deleted file mode 100644 index e69de29b..00000000 diff --git a/yesod-examples/Setup.hs b/yesod-examples/Setup.hs deleted file mode 100644 index cd7dc327..00000000 --- a/yesod-examples/Setup.hs +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env runhaskell -import Distribution.Simple -main = defaultMain diff --git a/yesod-examples/src/MkToForm2.hs b/yesod-examples/src/MkToForm2.hs deleted file mode 100644 index 1cedbeb5..00000000 --- a/yesod-examples/src/MkToForm2.hs +++ /dev/null @@ -1,15 +0,0 @@ -{-# LANGUAGE TypeFamilies, QuasiQuotes, GeneralizedNewtypeDeriving #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE MultiParamTypeClasses #-} -module MkToForm2 where - -import Yesod -import Data.Time (Day) - -mkPersist [$persist| -Entry - title String - day Day Desc toFormField=YesodJquery.jqueryDayField' - content Html toFormField=YesodNic.nicHtmlField - deriving -|] diff --git a/yesod-examples/src/ajax.lhs b/yesod-examples/src/ajax.lhs deleted file mode 100644 index 9396aef1..00000000 --- a/yesod-examples/src/ajax.lhs +++ /dev/null @@ -1,116 +0,0 @@ -We're going to write a very simple AJAX application. It will be a simple site with a few pages and a navbar; when you have Javascript, clicking on the links will load the pages via AJAX. Otherwise, it will use static HTML. - -We're going to use jQuery for the Javascript, though anything would work just fine. Also, the AJAX responses will be served as JSON. Let's get started. - -> {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} -> import Yesod -> import Yesod.Static -> import Data.Text (Text, unpack) - -Like the blog example, we'll define some data first. - -> data Page = Page -> { pageName :: Text -> , pageSlug :: Text -> , pageContent :: Text -> } - -> loadPages :: IO [Page] -> loadPages = return -> [ Page "Page 1" "page-1" "My first page" -> , Page "Page 2" "page-2" "My second page" -> , Page "Page 3" "page-3" "My third page" -> ] - -> data Ajax = Ajax -> { ajaxPages :: [Page] -> , ajaxStatic :: Static -> } - -Next we'll generate a function for each file in our static folder. This way, we get a compiler warning when trying to using a file which does not exist. - -> staticFiles "static/yesod/ajax" - -Now the routes; we'll have a homepage, a pattern for the pages, and use a static subsite for the Javascript and CSS files. - -> mkYesod "Ajax" [parseRoutes| -> / HomeR GET -> /page/#Text PageR GET -> /static StaticR Static ajaxStatic -> |] - -That third line there is the syntax for a subsite: Static is the datatype for the subsite argument; siteStatic returns the site itself (parse, render and dispatch functions); and ajaxStatic gets the subsite argument from the master argument. - -Now, we'll define the Yesod instance. We'll still use a dummy approot value, but we're also going to define a default layout. - -> instance Yesod Ajax where -> approot _ = "" -> defaultLayout widget = do -> Ajax pages _ <- getYesod -> content <- widgetToPageContent widget -> hamletToRepHtml [hamlet| -> \ -> -> -> -> #{pageTitle content} -> ->