{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, MultiParamTypeClasses, FlexibleInstances, TypeSynonymInstances, FlexibleContexts, TypeFamilies, RankNTypes, RecordWildCards, ScopedTypeVariables, UndecidableInstances, OverloadedStrings #-} module Clckwrks.Monad ( Clck , ClckPlugins , ClckT(..) , ClckForm , ClckFormT , ClckFormError(..) , ChildType(..) , ClckwrksConfig(..) , AttributeType(..) , Theme(..) , ThemeName , calcBaseURI , evalClckT , execClckT , runClckT , mapClckT , withRouteClckT , ClckState(..) , getUserId , Content(..) , markupToContent -- , addPreProcessor , addAdminMenu , addPreProc , setCurrentPage -- , getPrefix , getEnableAnalytics , getUnique , setUnique , requiresRole , requiresRole_ , query , update , nestURL , segments , transform ) where import Clckwrks.Admin.URL (AdminURL(..)) import Clckwrks.Acid (Acid(..), GetAcidState(..)) import Clckwrks.Page.Types (Markup(..), runPreProcessors) import Clckwrks.Menu.Acid (MenuState) import Clckwrks.Page.Acid (PageState, PageId) import Clckwrks.ProfileData.Acid (ProfileDataState, ProfileDataError(..), HasRole(..)) import Clckwrks.ProfileData.Types (Role(..)) import Clckwrks.Types (Prefix, Trust(Trusted)) import Clckwrks.Unauthorized (unauthorizedPage) import Clckwrks.URL (ClckURL(..)) import Control.Applicative (Alternative, Applicative, (<$>), (<|>), many) import Control.Monad (MonadPlus, foldM) import Control.Monad.State (MonadState, StateT, evalStateT, execStateT, get, mapStateT, modify, put, runStateT) import Control.Monad.Reader (MonadReader, ReaderT, mapReaderT) import Control.Monad.Trans (MonadIO(liftIO), lift) import Control.Concurrent.STM (TVar, readTVar, writeTVar, atomically) import Data.Aeson (Value(..)) import Data.Acid (AcidState, EventState, EventResult, QueryEvent, UpdateEvent) import Data.Acid.Advanced (query', update') import Data.Attoparsec.Text.Lazy (Parser, parseOnly, char, stringCI, try, takeWhile, takeWhile1) import qualified Data.HashMap.Lazy as HashMap import qualified Data.List as List import qualified Data.Map as Map import Data.Monoid (mappend, mconcat) import qualified Data.Text as Text import qualified Data.Vector as Vector import Data.ByteString.Lazy as LB (ByteString) import Data.ByteString.Lazy.UTF8 as LB (toString) import Data.Data (Data, Typeable) import Data.Map (Map) import Data.SafeCopy (SafeCopy(..)) import Data.Set (Set) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import Data.Text.Lazy.Builder (Builder, fromText) import qualified Data.Text.Lazy.Builder as B import Data.Time.Clock (UTCTime) import Data.Time.Format (formatTime) import Happstack.Auth (AuthProfileURL(..), AuthURL(..), AuthState, ProfileState, UserId) import qualified Happstack.Auth as Auth import Happstack.Server (Happstack, ServerMonad(..), FilterMonad(..), WebMonad(..), Input, Response, HasRqData(..), ServerPartT, UnWebT, mapServerPartT, escape) import Happstack.Server.HSP.HTML () -- ToMessage XML instance import Happstack.Server.Internal.Monads (FilterFun) import HSP hiding (Request, escape) import HSP.Google.Analytics (UACCT) import HSP.ServerPartT () import HSX.XMLGenerator (XMLGen(..)) import HSX.JMacro (IntegerSupply(..)) import Language.Javascript.JMacro import Prelude hiding (takeWhile) import System.Locale (defaultTimeLocale) import Text.Blaze.Html (Html) import Text.Blaze.Html.Renderer.String (renderHtml) import Text.Reform (CommonFormError, Form, FormError(..)) import Web.Routes (URL, MonadRoute(askRouteFn), RouteT(RouteT, unRouteT), mapRouteT, showURL, withRouteT) import Web.Plugins.Core (Plugins, getPluginsSt, modifyPluginsSt) import qualified Web.Routes as R import Web.Routes.Happstack (seeOtherURL) -- imported so that instances are scope even though we do not use them here import Web.Routes.XMLGenT () -- imported so that instances are scope even though we do not use them here type ClckPlugins = Plugins Theme (ClckT ClckURL (ServerPartT IO) Response) (ClckT ClckURL IO ()) ClckwrksConfig ([TL.Text -> ClckT ClckURL IO TL.Text]) type ThemeName = T.Text data Theme = Theme { themeName :: ThemeName , _themeTemplate :: ( EmbedAsChild (ClckT ClckURL (ServerPartT IO)) headers , EmbedAsChild (ClckT ClckURL (ServerPartT IO)) body) => T.Text -> headers -> body -> XMLGenT (ClckT ClckURL (ServerPartT IO)) XML , themeBlog :: XMLGenT (ClckT ClckURL (ServerPartT IO)) XML , themeDataDir :: IO FilePath } data ClckwrksConfig = ClckwrksConfig { clckHostname :: String -- ^ external name of the host , clckPort :: Int -- ^ port to listen on , clckHidePort :: Bool -- ^ hide port number in URL (useful when running behind a reverse proxy) , clckJQueryPath :: FilePath -- ^ path to @jquery.js@ on disk , clckJQueryUIPath :: FilePath -- ^ path to @jquery-ui.js@ on disk , clckJSTreePath :: FilePath -- ^ path to @jstree.js@ on disk , clckJSON2Path :: FilePath -- ^ path to @JSON2.js@ on disk , clckTopDir :: Maybe FilePath -- ^ path to top-level directory for all acid-state files/file uploads/etc , clckEnableAnalytics :: Bool -- ^ enable google analytics , clckInitHook :: T.Text -> ClckState -> ClckwrksConfig -> IO (ClckState, ClckwrksConfig) -- ^ init hook } -- | calculate the baseURI from the 'clckHostname', 'clckPort' and 'clckHidePort' options calcBaseURI :: ClckwrksConfig -> T.Text calcBaseURI c = Text.pack $ "http://" ++ (clckHostname c) ++ if ((clckPort c /= 80) && (clckHidePort c == False)) then (':' : show (clckPort c)) else "" data ClckState = ClckState { acidState :: Acid , currentPage :: PageId , uniqueId :: TVar Integer -- only unique for this request , adminMenus :: [(T.Text, [(T.Text, T.Text)])] , enableAnalytics :: Bool -- ^ enable Google Analytics , plugins :: ClckPlugins -- Plugins Theme (ClckT ClckURL (ServerPartT IO) Response) (ClckT ClckURL IO ()) ClckwrksConfig ([TL.Text -> ClckT ClckURL IO TL.Text]) } newtype ClckT url m a = ClckT { unClckT :: RouteT url (StateT ClckState m) a } deriving (Functor, Applicative, Alternative, Monad, MonadIO, MonadPlus, ServerMonad, HasRqData, FilterMonad r, WebMonad r, MonadState ClckState) instance (Happstack m) => Happstack (ClckT url m) -- | evaluate a 'ClckT' returning the inner monad -- -- similar to 'evalStateT'. evalClckT :: (Monad m) => (url -> [(Text.Text, Maybe Text.Text)] -> Text.Text) -- ^ function to act as 'showURLParams' -> ClckState -- ^ initial 'ClckState' -> ClckT url m a -- ^ 'ClckT' to evaluate -> m a evalClckT showFn clckState m = evalStateT (unRouteT (unClckT m) showFn) clckState -- | execute a 'ClckT' returning the final 'ClckState' -- -- similar to 'execStateT'. execClckT :: (Monad m) => (url -> [(Text.Text, Maybe Text.Text)] -> Text.Text) -- ^ function to act as 'showURLParams' -> ClckState -- ^ initial 'ClckState' -> ClckT url m a -- ^ 'ClckT' to evaluate -> m ClckState execClckT showFn clckState m = execStateT (unRouteT (unClckT m) showFn) clckState -- | run a 'ClckT' -- -- similar to 'runStateT'. runClckT :: (Monad m) => (url -> [(Text.Text, Maybe Text.Text)] -> Text.Text) -- ^ function to act as 'showURLParams' -> ClckState -- ^ initial 'ClckState' -> ClckT url m a -- ^ 'ClckT' to evaluate -> m (a, ClckState) runClckT showFn clckState m = runStateT (unRouteT (unClckT m) showFn) clckState -- | map a transformation function over the inner monad -- -- similar to 'mapStateT' mapClckT :: (m (a, ClckState) -> n (b, ClckState)) -- ^ transformation function -> ClckT url m a -- ^ initial monad -> ClckT url n b mapClckT f (ClckT r) = ClckT $ mapRouteT (mapStateT f) r -- | error returned when a reform 'Form' fails to validate data ClckFormError = ClckCFE (CommonFormError [Input]) | PDE ProfileDataError | EmptyUsername deriving (Show) instance FormError ClckFormError where type ErrorInputType ClckFormError = [Input] commonFormError = ClckCFE -- | ClckForm - type for reform forms type ClckFormT error m = Form m [Input] error [XMLGenT m XML] () type ClckForm url = Form (ClckT url (ServerPartT IO)) [Input] ClckFormError [XMLGenT (ClckT url (ServerPartT IO)) XML] () -- | update the 'currentPage' field of 'ClckState' setCurrentPage :: PageId -> Clck url () setCurrentPage pid = modify $ \s -> s { currentPage = pid } -- getPrefix :: Clck url Prefix -- getPrefix = componentPrefix <$> get setUnique :: Integer -> Clck url () setUnique i = do u <- uniqueId <$> get liftIO $ atomically $ writeTVar u i -- | get a unique 'Integer'. -- -- Only unique for the current request getUnique :: Clck url Integer getUnique = do u <- uniqueId <$> get liftIO $ atomically $ do i <- readTVar u writeTVar u (succ i) return i -- | get the 'Bool' value indicating if Google Analytics should be enabled or not getEnableAnalytics :: (Functor m, MonadState ClckState m) => m Bool getEnableAnalytics = enableAnalytics <$> get addAdminMenu :: (Monad m) => (T.Text, [(T.Text, T.Text)]) -> ClckT url m () addAdminMenu (category, entries) = modify $ \cs -> let oldMenus = adminMenus cs newMenus = Map.toAscList $ Map.insertWith List.union category entries $ Map.fromList oldMenus in cs { adminMenus = newMenus } -- | change the route url withRouteClckT :: ((url' -> [(T.Text, Maybe T.Text)] -> T.Text) -> url -> [(T.Text, Maybe T.Text)] -> T.Text) -> ClckT url m a -> ClckT url' m a withRouteClckT f (ClckT routeT) = (ClckT $ withRouteT f routeT) type Clck url = ClckT url (ServerPartT IO) instance IntegerSupply (Clck url) where nextInteger = getUnique instance ToJExpr Value where toJExpr (Object obj) = ValExpr $ JHash $ Map.fromList $ map (\(k,v) -> (T.unpack k, toJExpr v)) (HashMap.toList obj) toJExpr (Array vs) = ValExpr $ JList $ map toJExpr (Vector.toList vs) toJExpr (String s) = ValExpr $ JStr $ T.unpack s toJExpr (Number n) = ValExpr $ JDouble $ realToFrac n toJExpr (Bool True) = ValExpr $ JVar $ StrI "true" toJExpr (Bool False) = ValExpr $ JVar $ StrI "false" toJExpr Null = ValExpr $ JVar $ StrI "null" instance ToJExpr Text.Text where toJExpr t = ValExpr $ JStr $ T.unpack t nestURL :: (url1 -> url2) -> ClckT url1 m a -> ClckT url2 m a nestURL f (ClckT r) = ClckT $ R.nestURL f r instance (Monad m) => MonadRoute (ClckT url m) where type URL (ClckT url m) = url askRouteFn = ClckT $ askRouteFn -- | similar to the normal acid-state 'query' except it automatically gets the correct 'AcidState' handle from the environment query :: forall event m. (QueryEvent event, GetAcidState m (EventState event), Functor m, MonadIO m, MonadState ClckState m) => event -> m (EventResult event) query event = do as <- getAcidState query' (as :: AcidState (EventState event)) event -- | similar to the normal acid-state 'update' except it automatically gets the correct 'AcidState' handle from the environment update :: forall event m. (UpdateEvent event, GetAcidState m (EventState event), Functor m, MonadIO m, MonadState ClckState m) => event -> m (EventResult event) update event = do as <- getAcidState update' (as :: AcidState (EventState event)) event instance (GetAcidState m st) => GetAcidState (XMLGenT m) st where getAcidState = XMLGenT getAcidState instance (Functor m, Monad m) => GetAcidState (ClckT url m) AuthState where getAcidState = (acidAuth . acidState) <$> get instance (Functor m, Monad m) => GetAcidState (ClckT url m) ProfileState where getAcidState = (acidProfile . acidState) <$> get instance (Functor m, Monad m) => GetAcidState (ClckT url m) (MenuState ClckURL) where getAcidState = (acidMenu . acidState) <$> get instance (Functor m, Monad m) => GetAcidState (ClckT url m) PageState where getAcidState = (acidPage . acidState) <$> get instance (Functor m, Monad m) => GetAcidState (ClckT url m) ProfileDataState where getAcidState = (acidProfileData . acidState) <$> get -- | The the 'UserId' of the current user. While return 'Nothing' if they are not logged in. getUserId :: (Happstack m, GetAcidState m AuthState, GetAcidState m ProfileState) => m (Maybe UserId) getUserId = do authState <- getAcidState profileState <- getAcidState Auth.getUserId authState profileState -- * XMLGen / XMLGenerator instances for Clck instance (Functor m, Monad m) => XMLGen (ClckT url m) where type XMLType (ClckT url m) = XML newtype ChildType (ClckT url m) = ClckChild { unClckChild :: XML } newtype AttributeType (ClckT url m) = ClckAttr { unClckAttr :: Attribute } genElement n attrs children = do attribs <- map unClckAttr <$> asAttr attrs childer <- flattenCDATA . map (unClckChild) <$> asChild children XMLGenT $ return (Element (toName n) attribs childer ) xmlToChild = ClckChild pcdataToChild = xmlToChild . pcdata flattenCDATA :: [XML] -> [XML] flattenCDATA cxml = case flP cxml [] of [] -> [] [CDATA _ ""] -> [] xs -> xs where flP :: [XML] -> [XML] -> [XML] flP [] bs = reverse bs flP [x] bs = reverse (x:bs) flP (x:y:xs) bs = case (x,y) of (CDATA e1 s1, CDATA e2 s2) | e1 == e2 -> flP (CDATA e1 (s1++s2) : xs) bs _ -> flP (y:xs) (x:bs) instance (Functor m, Monad m) => IsAttrValue (ClckT url m) T.Text where toAttrValue = toAttrValue . T.unpack instance (Functor m, Monad m) => IsAttrValue (ClckT url m) TL.Text where toAttrValue = toAttrValue . TL.unpack instance (Functor m, Monad m) => EmbedAsAttr (ClckT url m) Attribute where asAttr = return . (:[]) . ClckAttr instance (Functor m, Monad m, IsName n) => EmbedAsAttr (ClckT url m) (Attr n String) where asAttr (n := str) = asAttr $ MkAttr (toName n, pAttrVal str) instance (Functor m, Monad m, IsName n) => EmbedAsAttr (ClckT url m) (Attr n Char) where asAttr (n := c) = asAttr (n := [c]) instance (Functor m, Monad m, IsName n) => EmbedAsAttr (ClckT url m) (Attr n Bool) where asAttr (n := True) = asAttr $ MkAttr (toName n, pAttrVal "true") asAttr (n := False) = asAttr $ MkAttr (toName n, pAttrVal "false") instance (Functor m, Monad m, IsName n) => EmbedAsAttr (ClckT url m) (Attr n Int) where asAttr (n := i) = asAttr $ MkAttr (toName n, pAttrVal (show i)) instance (Functor m, Monad m, IsName n) => EmbedAsAttr (ClckT url m) (Attr n Integer) where asAttr (n := i) = asAttr $ MkAttr (toName n, pAttrVal (show i)) instance (IsName n) => EmbedAsAttr (Clck ClckURL) (Attr n ClckURL) where asAttr (n := u) = do url <- showURL u asAttr $ MkAttr (toName n, pAttrVal (T.unpack url)) instance (IsName n) => EmbedAsAttr (Clck AdminURL) (Attr n AdminURL) where asAttr (n := u) = do url <- showURL u asAttr $ MkAttr (toName n, pAttrVal (T.unpack url)) {- instance EmbedAsAttr Clck (Attr String AuthURL) where asAttr (n := u) = do url <- showURL (W_Auth u) asAttr $ MkAttr (toName n, pAttrVal url) -} instance (Functor m, Monad m, IsName n) => (EmbedAsAttr (ClckT url m) (Attr n TL.Text)) where asAttr (n := a) = asAttr $ MkAttr (toName n, pAttrVal $ TL.unpack a) instance (Functor m, Monad m, IsName n) => (EmbedAsAttr (ClckT url m) (Attr n T.Text)) where asAttr (n := a) = asAttr $ MkAttr (toName n, pAttrVal $ T.unpack a) instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Char where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata . (:[]) instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) String where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Int where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata . show instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Integer where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata . show instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Double where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata . show instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Float where asChild = XMLGenT . return . (:[]) . ClckChild . pcdata . show instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) TL.Text where asChild = asChild . TL.unpack instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) T.Text where asChild = asChild . T.unpack instance (EmbedAsChild (ClckT url1 m) a, url1 ~ url2) => EmbedAsChild (ClckT url1 m) (ClckT url2 m a) where asChild c = do a <- XMLGenT c asChild a instance (Functor m, MonadIO m, EmbedAsChild (ClckT url m) a) => EmbedAsChild (ClckT url m) (IO a) where asChild c = do a <- XMLGenT (liftIO c) asChild a {- instance EmbedAsChild Clck TextHtml where asChild = XMLGenT . return . (:[]) . ClckChild . cdata . T.unpack . unTextHtml -} instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) XML where asChild = XMLGenT . return . (:[]) . ClckChild instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Html where asChild = XMLGenT . return . (:[]) . ClckChild . cdata . renderHtml instance (Functor m, Monad m, Happstack m) => EmbedAsChild (ClckT url m) Markup where asChild mrkup = asChild =<< (XMLGenT $ markupToContent mrkup) instance (Functor m, MonadIO m, Happstack m) => EmbedAsChild (ClckT url m) ClckFormError where asChild formError = asChild (show formError) instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) () where asChild () = return [] instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) UTCTime where asChild = asChild . formatTime defaultTimeLocale "%a, %F @ %r" instance (Functor m, Monad m, EmbedAsChild (ClckT url m) a) => EmbedAsChild (ClckT url m) (Maybe a) where asChild Nothing = asChild () asChild (Just a) = asChild a instance (Functor m, Monad m) => AppendChild (ClckT url m) XML where appAll xml children = do chs <- children case xml of CDATA _ _ -> return xml Element n as cs -> return $ Element n as (cs ++ (map unClckChild chs)) instance (Functor m, Monad m) => SetAttr (ClckT url m) XML where setAll xml hats = do attrs <- hats case xml of CDATA _ _ -> return xml Element n as cs -> return $ Element n (foldr (:) as (map unClckAttr attrs)) cs instance (Functor m, Monad m) => XMLGenerator (ClckT url m) -- | a wrapper which identifies how to treat different 'Text' values when attempting to embed them. -- -- In general 'Content' values have already been -- flatten/preprocessed/etc and are now basic formats like -- @text/plain@, @text/html@, etc data Content = TrustedHtml T.Text | PlainText T.Text deriving (Eq, Ord, Read, Show, Data, Typeable) instance (Functor m, Monad m) => EmbedAsChild (ClckT url m) Content where asChild (TrustedHtml html) = asChild $ cdata (T.unpack html) asChild (PlainText txt) = asChild $ pcdata (T.unpack txt) -- | convert 'Markup' to 'Content' that can be embedded. Generally by running the pre-processors needed. -- markupToContent :: (Functor m, MonadIO m, Happstack m) => Markup -> ClckT url m Content markupToContent :: (Functor m, MonadIO m, Happstack m) => Markup -> ClckT url m Content markupToContent Markup{..} = do clckState <- get transformers <- liftIO $ getPluginsSt (plugins clckState) (markup', clckState') <- liftIO $ runClckT undefined clckState (foldM (\txt pp -> pp txt) (TL.fromStrict markup) transformers) put clckState' e <- liftIO $ runPreProcessors preProcessors trust (TL.toStrict markup') case e of (Left err) -> return (PlainText err) (Right html) -> return (TrustedHtml html) addPreProc :: (MonadIO m) => Plugins theme n hook config [TL.Text -> ClckT ClckURL IO TL.Text] -> (TL.Text -> ClckT ClckURL IO TL.Text) -> m () addPreProc plugins p = modifyPluginsSt plugins $ \ps -> p : ps -- * Preprocess data Segment cmd = TextBlock T.Text | Cmd cmd deriving Show instance Functor Segment where fmap f (TextBlock t) = TextBlock t fmap f (Cmd c) = Cmd (f c) transform :: (Monad m) => (cmd -> m Builder) -> [Segment cmd] -> m Builder transform f segments = do bs <- mapM (transformSegment f) segments return (mconcat bs) transformSegment :: (Monad m) => (cmd -> m Builder) -> Segment cmd -> m Builder transformSegment f (TextBlock t) = return (B.fromText t) transformSegment f (Cmd cmd) = f cmd segments :: T.Text -> Parser a -> Parser [Segment a] segments name p = many (cmd name p <|> plainText) cmd :: T.Text -> Parser cmd -> Parser (Segment cmd) cmd n p = do char '{' ((try $ do stringCI n char '|' r <- p char '}' return (Cmd r)) <|> (do t <- takeWhile1 (/= '{') return $ TextBlock (T.cons '{' t))) plainText :: Parser (Segment cmd) plainText = do t <- takeWhile1 (/= '{') return $ TextBlock t -- * Require Role requiresRole_ :: (Happstack m) => (ClckURL -> [(T.Text, Maybe T.Text)] -> T.Text) -> Set Role -> url -> ClckT u m url requiresRole_ showFn role url = ClckT $ RouteT $ \_ -> unRouteT (unClckT (requiresRole role url)) showFn requiresRole :: (Happstack m) => Set Role -> url -> ClckT ClckURL m url requiresRole role url = do mu <- getUserId case mu of Nothing -> escape $ seeOtherURL (Auth $ AuthURL A_Login) (Just uid) -> do r <- query (HasRole uid role) if r then return url else escape $ unauthorizedPage ("You do not have permission to view this page." :: T.Text)