{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module Hledger.Web.App where
import Control.Applicative ((<|>))
import Control.Monad (join, when, unless)
import qualified Data.ByteString.Char8 as BC
import Data.Traversable (for)
import Data.IORef (IORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Calendar (Day)
import Network.HTTP.Conduit (Manager)
import Network.HTTP.Types (status403)
import Network.Wai (requestHeaders)
import System.Directory (XdgDirectory (..), createDirectoryIfMissing,
getXdgDirectory)
import System.FilePath (takeFileName, (</>))
import Text.Blaze (Markup)
import Text.Hamlet (hamletFile)
import Yesod
import Yesod.Static
import Yesod.Default.Config
#ifndef DEVELOPMENT
import Hledger.Web.Settings (staticDir)
import Text.Jasmine (minifym)
import Yesod.Default.Util (addStaticContentExternal)
#endif
import Hledger
import Hledger.Cli (CliOpts(..), journalReloadIfChanged)
import Hledger.Web.Settings (Extra(..), widgetFile)
import Hledger.Web.Settings.StaticFiles
import Hledger.Web.WebOptions
import Hledger.Web.Widget.Common (balanceReportAsHtml)
import Data.List (isPrefixOf)
data App = App
{ App -> AppConfig DefaultEnv Extra
settings :: AppConfig DefaultEnv Extra
, App -> Static
getStatic :: Static
, App -> Manager
httpManager :: Manager
, App -> WebOpts
appOpts :: WebOpts
, App -> IORef Journal
appJournal :: IORef Journal
}
mkYesodData "App" $(parseRoutesFile "config/routes")
type AppRoute = Route App
type Form a = Html -> MForm Handler (FormResult a, Widget)
instance Yesod App where
approot :: Approot App
approot
| Bool
hasbaseurl = (App -> Text) -> Approot App
forall master. (master -> Text) -> Approot master
ApprootMaster (String -> Text
T.pack (String -> Text) -> (App -> String) -> App -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. WebOpts -> String
base_url_ (WebOpts -> String) -> (App -> WebOpts) -> App -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. App -> WebOpts
appOpts)
| Bool
otherwise = Approot App -> Approot App
forall site. Approot site -> Approot site
guessApprootOr ((App -> Text) -> Approot App
forall master. (master -> Text) -> Approot master
ApprootMaster (AppConfig DefaultEnv Extra -> Text
forall environment extra. AppConfig environment extra -> Text
appRoot (AppConfig DefaultEnv Extra -> Text)
-> (App -> AppConfig DefaultEnv Extra) -> App -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. App -> AppConfig DefaultEnv Extra
settings))
where
hasbaseurl :: Bool
hasbaseurl = (String -> Bool) -> [String] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (String
"--base-url" String -> String -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isPrefixOf`) [String]
progArgs
makeSessionBackend :: App -> IO (Maybe SessionBackend)
makeSessionBackend App
_ = do
hledgerdata <- XdgDirectory -> String -> IO String
getXdgDirectory XdgDirectory
XdgCache String
"hledger"
createDirectoryIfMissing True hledgerdata
let sessionexpirysecs = Int
120
Just <$> defaultClientSessionBackend sessionexpirysecs (hledgerdata </> "hledger-web_client_session_key.aes")
defaultLayout :: WidgetFor App () -> HandlerFor App Html
defaultLayout WidgetFor App ()
widget = do
Handler ()
checkServerSideUiEnabled
master <- HandlerFor App (HandlerSite (HandlerFor App))
HandlerFor App App
forall (m :: * -> *). MonadHandler m => m (HandlerSite m)
getYesod
here <- fromMaybe RootR <$> getCurrentRoute
VD{opts, j, qparam, q, qopts, perms} <- getViewData
msg <- getMessage
showSidebar <- shouldShowSidebar
let rspec = CliOpts -> ReportSpec
reportspec_ (WebOpts -> CliOpts
cliopts_ WebOpts
opts)
ropts = ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec
ropts' = (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec)
{accountlistmode_ = ALTree
,empty_ = True
}
rspec' = ReportSpec
rspec{_rsQuery=q,_rsReportOpts=ropts'}
hideEmptyAccts <- if empty_ ropts
then return True
else (== Just "1") . lookup "hideemptyaccts" . reqCookies <$> getRequest
let accounts =
(Route App, Route App)
-> Route App
-> Bool
-> Journal
-> Text
-> [QueryOpt]
-> BalanceReport
-> HtmlUrl (Route App)
forall r.
Eq r =>
(r, r)
-> r
-> Bool
-> Journal
-> Text
-> [QueryOpt]
-> BalanceReport
-> HtmlUrl r
balanceReportAsHtml (Route App
JournalR, Route App
RegisterR) Route App
here Bool
hideEmptyAccts Journal
j Text
qparam [QueryOpt]
qopts (BalanceReport -> HtmlUrl (Route App))
-> BalanceReport -> HtmlUrl (Route App)
forall a b. (a -> b) -> a -> b
$
Map Text AmountStyle -> BalanceReport -> BalanceReport
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts (Rounding -> Journal -> Map Text AmountStyle
journalCommodityStylesWith Rounding
HardRounding Journal
j) (BalanceReport -> BalanceReport) -> BalanceReport -> BalanceReport
forall a b. (a -> b) -> a -> b
$
ReportSpec -> Journal -> BalanceReport
balanceReport ReportSpec
rspec' Journal
j
topShowmd = if Bool
showSidebar then Text
"col-md-4" else Text
"col-any-0" :: Text
topShowsm = if Bool
showSidebar then Text
"col-sm-4" else Text
"" :: Text
sideShowmd = if Bool
showSidebar then Text
"col-md-4" else Text
"col-any-0" :: Text
sideShowsm = if Bool
showSidebar then Text
"col-sm-4" else Text
"" :: Text
mainShowmd = if Bool
showSidebar then Text
"col-md-8" else Text
"col-md-12" :: Text
mainShowsm = if Bool
showSidebar then Text
"col-sm-8" else Text
"col-sm-12" :: Text
pc <- widgetToPageContent $ do
addStylesheet $ StaticR css_bootstrap_min_css
addStylesheet $ StaticR css_bootstrap_datepicker_standalone_min_css
toWidgetHead [hamlet|
<script type="text/javascript" src="@{StaticR js_jquery_min_js}">
<script type="text/javascript" src="@{StaticR js_typeahead_bundle_min_js}">
|]
addScript $ StaticR js_bootstrap_min_js
addScript $ StaticR js_bootstrap_datepicker_min_js
addScript $ StaticR js_jquery_url_js
addScript $ StaticR js_jquery_cookie_js
addScript $ StaticR js_jquery_hotkeys_js
addScript $ StaticR js_jquery_flot_min_js
addScript $ StaticR js_jquery_flot_selection_min_js
addScript $ StaticR js_jquery_flot_time_min_js
addScript $ StaticR js_jquery_flot_tooltip_min_js
toWidget [hamlet| \<!--[if lte IE 8]> <script type="text/javascript" src="@{StaticR js_excanvas_min_js}"></script> <![endif]--> |]
addStylesheet $ StaticR hledger_css
addScript $ StaticR hledger_js
$(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
#ifndef DEVELOPMENT
addStaticContent :: Text
-> Text
-> ByteString
-> HandlerFor App (Maybe (Either Text (Route App, [(Text, Text)])))
addStaticContent = (ByteString -> Either String ByteString)
-> (ByteString -> String)
-> String
-> ([Text] -> Route App)
-> Text
-> Text
-> ByteString
-> HandlerFor App (Maybe (Either Text (Route App, [(Text, Text)])))
forall a master.
(ByteString -> Either a ByteString)
-> (ByteString -> String)
-> String
-> ([Text] -> Route master)
-> Text
-> Text
-> ByteString
-> HandlerFor
master (Maybe (Either Text (Route master, [(Text, Text)])))
addStaticContentExternal ByteString -> Either String ByteString
minifym ByteString -> String
base64md5 String
staticDir (Route Static -> Route App
StaticR (Route Static -> Route App)
-> ([Text] -> Route Static) -> [Text] -> Route App
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([Text] -> [(Text, Text)] -> Route Static)
-> [(Text, Text)] -> [Text] -> Route Static
forall a b c. (a -> b -> c) -> b -> a -> c
flip [Text] -> [(Text, Text)] -> Route Static
StaticRoute [])
#endif
instance RenderMessage App FormMessage where
renderMessage :: App -> [Text] -> FormMessage -> Text
renderMessage App
_ [Text]
_ = FormMessage -> Text
defaultFormMessage
data ViewData = VD
{ ViewData -> WebOpts
opts :: WebOpts
, ViewData -> Day
today :: Day
, ViewData -> Journal
j :: Journal
, ViewData -> Text
qparam :: Text
, ViewData -> Query
q :: Query
, ViewData -> [QueryOpt]
qopts :: [QueryOpt]
, ViewData -> [Permission]
perms :: [Permission]
} deriving (Int -> ViewData -> ShowS
[ViewData] -> ShowS
ViewData -> String
(Int -> ViewData -> ShowS)
-> (ViewData -> String) -> ([ViewData] -> ShowS) -> Show ViewData
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ViewData -> ShowS
showsPrec :: Int -> ViewData -> ShowS
$cshow :: ViewData -> String
show :: ViewData -> String
$cshowList :: [ViewData] -> ShowS
showList :: [ViewData] -> ShowS
Show)
instance Show Text.Blaze.Markup where show :: Html -> String
show Html
_ = String
"<blaze markup>"
getViewData :: Handler ViewData
getViewData :: Handler ViewData
getViewData = do
App{
appOpts=opts@WebOpts{ cliopts_=copts@CliOpts{ reportspec_=rspec@ReportSpec{_rsReportOpts, _rsQuery} } },
appJournal
} <- HandlerFor App (HandlerSite (HandlerFor App))
HandlerFor App App
forall (m :: * -> *). MonadHandler m => m (HandlerSite m)
getYesod
let today = ReportSpec -> Day
_rsDay ReportSpec
rspec
(j, mjerr) <- getCurrentJournal
appJournal
copts{reportspec_=rspec{_rsReportOpts=_rsReportOpts{no_elide_=True}}}
today
qparam <- fromMaybe "" <$> lookupGetParam "q"
(q1, qopts, mqerr) <- do
case parseQuery today qparam of
Right (Query
q0, [QueryOpt]
qopts) -> (Query, [QueryOpt], Maybe String)
-> HandlerFor App (Query, [QueryOpt], Maybe String)
forall a. a -> HandlerFor App a
forall (m :: * -> *) a. Monad m => a -> m a
return (Query
q0, [QueryOpt]
qopts, Maybe String
forall a. Maybe a
Nothing)
Left String
err -> (Query, [QueryOpt], Maybe String)
-> HandlerFor App (Query, [QueryOpt], Maybe String)
forall a. a -> HandlerFor App a
forall (m :: * -> *) a. Monad m => a -> m a
return (Query
Any, [], String -> Maybe String
forall a. a -> Maybe a
Just String
err)
let
initialdepthq = (Query -> Bool) -> Query -> Query
filterQuery Query -> Bool
queryIsDepth Query
_rsQuery
q = Query -> Query
simplifyQuery (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ [Query] -> Query
And [Query
q1, Query
initialdepthq]
maybe (pure ()) (setMessage . toHtml) $ mjerr <|> mqerr
perms <- case allow_ opts of
AccessLevel
SandstormAccess -> do
let h :: HeaderName
h = HeaderName
"X-Sandstorm-Permissions"
hs <- ((HeaderName, ByteString) -> [ByteString])
-> [(HeaderName, ByteString)] -> [[ByteString]]
forall a b. (a -> b) -> [a] -> [b]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Char -> ByteString -> [ByteString]
BC.split Char
',' (ByteString -> [ByteString])
-> ((HeaderName, ByteString) -> ByteString)
-> (HeaderName, ByteString)
-> [ByteString]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (HeaderName, ByteString) -> ByteString
forall a b. (a, b) -> b
snd) ([(HeaderName, ByteString)] -> [[ByteString]])
-> (Request -> [(HeaderName, ByteString)])
-> Request
-> [[ByteString]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((HeaderName, ByteString) -> Bool)
-> [(HeaderName, ByteString)] -> [(HeaderName, ByteString)]
forall a. (a -> Bool) -> [a] -> [a]
filter ((HeaderName -> HeaderName -> Bool
forall a. Eq a => a -> a -> Bool
== HeaderName
h) (HeaderName -> Bool)
-> ((HeaderName, ByteString) -> HeaderName)
-> (HeaderName, ByteString)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (HeaderName, ByteString) -> HeaderName
forall a b. (a, b) -> a
fst) ([(HeaderName, ByteString)] -> [(HeaderName, ByteString)])
-> (Request -> [(HeaderName, ByteString)])
-> Request
-> [(HeaderName, ByteString)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Request -> [(HeaderName, ByteString)]
requestHeaders (Request -> [[ByteString]])
-> HandlerFor App Request -> HandlerFor App [[ByteString]]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> HandlerFor App Request
forall (m :: * -> *). MonadHandler m => m Request
waiRequest
fmap join . for (join hs) $ \ByteString
x -> case ByteString -> Either Text Permission
parsePermission ByteString
x of
Left Text
e -> [] [Permission] -> Handler () -> HandlerFor App [Permission]
forall a b. a -> HandlerFor App b -> HandlerFor App a
forall (f :: * -> *) a b. Functor f => a -> f b -> f a
<$ Text -> Html -> Handler ()
forall (m :: * -> *). MonadHandler m => Text -> Html -> m ()
addMessage Text
"" (Html
"Unknown permission: " Html -> Html -> Html
forall a. Semigroup a => a -> a -> a
<> Text -> Html
forall a. ToMarkup a => a -> Html
toHtml Text
e)
Right Permission
p -> [Permission] -> HandlerFor App [Permission]
forall a. a -> HandlerFor App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure [Permission
p]
AccessLevel
cliaccess -> [Permission] -> HandlerFor App [Permission]
forall a. a -> HandlerFor App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Permission] -> HandlerFor App [Permission])
-> [Permission] -> HandlerFor App [Permission]
forall a b. (a -> b) -> a -> b
$ AccessLevel -> [Permission]
accessLevelToPermissions AccessLevel
cliaccess
return VD{opts, today, j, qparam, q, qopts, perms}
checkServerSideUiEnabled :: Handler ()
checkServerSideUiEnabled :: Handler ()
checkServerSideUiEnabled = do
VD{opts=WebOpts{server_mode_}} <- Handler ViewData
getViewData
when (server_mode_ == ServeJson) $
sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text)
shouldShowSidebar :: Handler Bool
= do
msidebarparam <- Text -> HandlerFor App (Maybe Text)
forall (m :: * -> *). MonadHandler m => Text -> m (Maybe Text)
lookupGetParam Text
"sidebar"
msidebarcookie <- lookup "showsidebar" . reqCookies <$> getRequest
return $
let disablevalues = [Text
"",Text
"0"]
in maybe True (`notElem` disablevalues) $ msidebarparam <|> msidebarcookie
getCurrentJournal :: IORef Journal -> CliOpts -> Day -> Handler (Journal, Maybe String)
getCurrentJournal :: IORef Journal -> CliOpts -> Day -> Handler (Journal, Maybe String)
getCurrentJournal IORef Journal
jref CliOpts
opts Day
d = do
let depthlessinitialq :: Query
depthlessinitialq = (Query -> Bool) -> Query -> Query
filterQuery (Bool -> Bool
not (Bool -> Bool) -> (Query -> Bool) -> Query -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> Bool
queryIsDepth) (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Query
_rsQuery (ReportSpec -> Query) -> ReportSpec -> Query
forall a b. (a -> b) -> a -> b
$ CliOpts -> ReportSpec
reportspec_ CliOpts
opts
j <- IO Journal -> HandlerFor App Journal
forall a. IO a -> HandlerFor App a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IORef Journal -> IO Journal
forall a. IORef a -> IO a
readIORef IORef Journal
jref)
ej <- liftIO . runExceptT $ journalReloadIfChanged opts d j
case ej of
Left String
e -> do
Html -> Handler ()
forall (m :: * -> *). MonadHandler m => Html -> m ()
setMessage Html
"error while reading journal"
(Journal, Maybe String) -> Handler (Journal, Maybe String)
forall a. a -> HandlerFor App a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal
j, String -> Maybe String
forall a. a -> Maybe a
Just String
e)
Right (Journal
j', Bool
True) -> do
IO () -> Handler ()
forall a. IO a -> HandlerFor App a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> Handler ())
-> (Journal -> IO ()) -> Journal -> Handler ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. IORef Journal -> Journal -> IO ()
forall a. IORef a -> a -> IO ()
writeIORef IORef Journal
jref (Journal -> Handler ()) -> Journal -> Handler ()
forall a b. (a -> b) -> a -> b
$ Query -> Journal -> Journal
filterJournalTransactions Query
depthlessinitialq Journal
j'
(Journal, Maybe String) -> Handler (Journal, Maybe String)
forall a. a -> HandlerFor App a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal
j',Maybe String
forall a. Maybe a
Nothing)
Right (Journal
_, Bool
False) -> (Journal, Maybe String) -> Handler (Journal, Maybe String)
forall a. a -> HandlerFor App a
forall (m :: * -> *) a. Monad m => a -> m a
return (Journal
j, Maybe String
forall a. Maybe a
Nothing)
require :: Permission -> Handler ()
require :: Permission -> Handler ()
require Permission
p = do
VD{perms} <- Handler ViewData
getViewData
unless (p `elem` perms) $ permissionDenied $
"Missing the '" <> T.pack (showPermission p) <> "' permission"