{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ViewPatterns #-}
module Hledger.Web.Foundation where
import Control.Monad (join, when)
import qualified Data.ByteString.Char8 as BC
import Data.Traversable (for)
import Data.IORef (IORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe)
#if !(MIN_VERSION_base(4,13,0))
import Data.Monoid ((<>))
#endif
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.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)
data App = App
{ settings :: AppConfig DefaultEnv Extra
, getStatic :: Static
, httpManager :: Manager
, appOpts :: WebOpts
, appJournal :: IORef Journal
}
mkYesodData "App" $(parseRoutesFile "config/routes")
type AppRoute = Route App
#if MIN_VERSION_yesod(1,6,0)
type Form x = Html -> MForm (HandlerFor App) (FormResult x, Widget)
#else
type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget)
#endif
instance Yesod App where
approot = ApprootMaster $ appRoot . settings
makeSessionBackend _ =
let sessionexpirysecs = 120
in Just <$> defaultClientSessionBackend sessionexpirysecs ".hledger-web_client_session_key.aes"
defaultLayout widget = do
checkServerSideUiEnabled
master <- getYesod
here <- fromMaybe RootR <$> getCurrentRoute
VD {caps, j, m, opts, q, qopts} <- getViewData
msg <- getMessage
showSidebar <- shouldShowSidebar
hideEmptyAccts <- (== Just "1") . lookup "hideemptyaccts" . reqCookies <$> getRequest
let ropts = reportopts_ (cliopts_ opts)
ropts' = ropts { empty_ = not (empty_ ropts) }
accounts =
balanceReportAsHtml (JournalR, RegisterR) here hideEmptyAccts j q qopts $
balanceReport ropts' m j
topShowmd = if showSidebar then "col-md-4" else "col-any-0" :: Text
topShowsm = if showSidebar then "col-sm-4" else "" :: Text
sideShowmd = if showSidebar then "col-md-4" else "col-any-0" :: Text
sideShowsm = if showSidebar then "col-sm-4" else "" :: Text
mainShowmd = if showSidebar then "col-md-8" else "col-md-12" :: Text
mainShowsm = if showSidebar then "col-sm-8" else "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_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 = addStaticContentExternal minifym base64md5 staticDir (StaticR . flip StaticRoute [])
#endif
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
data ViewData = VD
{ opts :: WebOpts
, today :: Day
, j :: Journal
, q :: Text
, m :: Query
, qopts :: [QueryOpt]
, caps :: [Capability]
} deriving (Show)
instance Show Text.Blaze.Markup where show _ = "<blaze markup>"
getViewData :: Handler ViewData
getViewData = do
App {appOpts = opts, appJournal} <- getYesod
today <- liftIO getCurrentDay
let copts = cliopts_ opts
(j, merr) <-
getCurrentJournal
appJournal
copts {reportopts_ = (reportopts_ copts) {no_elide_ = True}}
today
maybe (pure ()) (setMessage . toHtml) merr
q <- fromMaybe "" <$> lookupGetParam "q"
let (m, qopts) = parseQuery today q
caps <- case capabilitiesHeader_ opts of
Nothing -> return (capabilities_ opts)
Just h -> do
hs <- fmap (BC.split ',' . snd) . filter ((== h) . fst) . requestHeaders <$> waiRequest
fmap join . for (join hs) $ \x -> case capabilityFromBS x of
Left e -> [] <$ addMessage "" ("Unknown permission: " <> toHtml (BC.unpack e))
Right c -> pure [c]
return VD {opts, today, j, q, m, qopts, caps}
checkServerSideUiEnabled :: Handler ()
checkServerSideUiEnabled = do
VD{opts=WebOpts{serve_api_}} <- getViewData
when serve_api_ $
sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text)
shouldShowSidebar :: Handler Bool
shouldShowSidebar = do
msidebarparam <- lookupGetParam "sidebar"
msidebarcookie <- lookup "showsidebar" . reqCookies <$> getRequest
return $
let disablevalues = ["","0"]
in maybe
(not $ msidebarcookie `elem` map Just disablevalues)
(not . (`elem` disablevalues))
msidebarparam
getCurrentJournal :: IORef Journal -> CliOpts -> Day -> Handler (Journal, Maybe String)
getCurrentJournal jref opts d = do
j <- liftIO (readIORef jref)
(ej, changed) <- liftIO $ journalReloadIfChanged opts d j
let initq = queryFromOpts d (reportopts_ opts)
case (changed, filterJournalTransactions initq <$> ej) of
(False, _) -> return (j, Nothing)
(True, Right j') -> do
liftIO $ writeIORef jref j'
return (j',Nothing)
(True, Left e) -> do
setMessage "error while reading journal"
return (j, Just e)