Foundation.hs 8.4 KB
Newer Older
Christopher League's avatar
Christopher League committed
1 2 3 4 5 6 7 8 9 10
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE InstanceSigs          #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
11

Christopher League's avatar
Christopher League committed
12 13 14 15 16 17 18 19 20
module Foundation
  ( App(..)
  , Handler
  , Widget
  , Form
  , Route(..)
  , resourcesApp
  , unsafeHandler
  ) where
21

22 23
import qualified Control.Monad.Catch  as MC
import           Control.Monad.Logger (LogSource)
24
import qualified Data.CaseInsensitive as CI
25
import qualified Data.Text.Encoding   as TE
26
import           Data.Time.Clock      (NominalDiffTime)
27 28 29 30 31 32
import           Import.NoFoundation  as Pre
import           Text.Hamlet          (hamletFile)
import           Text.Jasmine         (minifym)
import           Yesod.Core.Types     (Logger)
import qualified Yesod.Core.Unsafe    as Unsafe
import           Yesod.Default.Util   (addStaticContentExternal)
33 34 35 36 37 38

-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
-- access to the data present here.
data App = App
39 40
  { appSettings    :: AppSettings
  , appStatic      :: Static -- ^ Settings for static file serving.
41
  , appHttpManager :: Manager
42 43 44
  , appLogger      :: Logger
  , appCalendarCxt :: CalendarContext
  , appGetCalendar :: Maybe NominalDiffTime -> HandlerFor App [SimpleEventUTC]
45 46 47
    -- ^ Fetch latest available times from the calendars. Return cached
    -- version if it's less than given age, which defaults to
    -- 'appCacheExpiry'.
48 49
  }

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
-- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see:
-- http://www.yesodweb.com/book/routing-and-handlers
--
-- Note that this is really half the story; in Application.hs, mkYesodDispatch
-- generates the rest of the code. Please see the following documentation
-- for an explanation for this split:
-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules
--
-- This function also generates the following type synonyms:
-- type Handler = HandlerT App IO
-- type Widget = WidgetT App IO ()
mkYesodData "App" $(parseRoutesFile "config/routes")

-- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerFor App) (FormResult x, Widget)

67 68 69
instance MC.MonadCatch (HandlerFor App) where
  catch = Pre.catch

70 71 72
addStylesheetL :: Route App -> Text -> [(Text, Text)] -> Widget
addStylesheetL route cdn attrs =
  appLocalAssets . appSettings <$> getYesod >>= \case
73 74
    True -> addStylesheet route
    False -> addStylesheetRemoteAttrs cdn attrs
75 76 77 78

addScriptL :: Route App -> Text -> [(Text, Text)] -> Widget
addScriptL route cdn attrs =
  appLocalAssets . appSettings <$> getYesod >>= \case
79 80
    True -> addScript route
    False -> addScriptRemoteAttrs cdn attrs
81

82 83
-- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here.
Christopher League's avatar
Christopher League committed
84
instance Yesod App where
85 86 87 88 89 90
  approot :: Approot App
  approot =
    ApprootRequest $ \app req ->
      case appRoot $ appSettings app of
        Nothing   -> getApprootText guessApproot app req
        Just root -> root
91 92
    -- Store session data on the client in encrypted cookies,
    -- default session idle timeout is 120 minutes
93 94 95 96 97 98
  makeSessionBackend :: App -> IO (Maybe SessionBackend)
  makeSessionBackend _ =
    Just <$>
    defaultClientSessionBackend
      120 -- timeout in minutes
      "config/client_session_key.aes"
99 100 101 102 103 104 105
    -- Yesod Middleware allows you to run code before and after each handler function.
    -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks.
    -- Some users may also want to add the defaultCsrfMiddleware, which:
    --   a) Sets a cookie with a CSRF token in it.
    --   b) Validates that incoming write requests include that token in either a header or POST parameter.
    -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
    -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
106 107 108 109 110 111 112 113
  yesodMiddleware :: ToTypedContent res => Handler res -> Handler res
  yesodMiddleware = defaultYesodMiddleware
  defaultLayout :: Widget -> Handler Html
  defaultLayout widget = do
    master <- getYesod
    mmsg <- getMessage
    pc <-
      widgetToPageContent $ do
114 115
        let fontFamily =
              asText "'Play', 'Helvetica Neue', Helvetica, Arial, sans-serif"
116 117
        addScriptL
          (StaticR js_jquery_3_3_1_min_js)
118
          "https://code.jquery.com/jquery-3.3.1.min.js"
119 120
          [ ("integrity", "sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=")
          , ("crossorigin", "anonymous")
121
          ]
122 123
        addScriptL
          (StaticR js_js_cookie_2_2_0_min_js)
124 125
          "https://cdn.jsdelivr.net/npm/js-cookie@2.2.0/src/js.cookie.min.js"
          []
126 127 128 129
        addStylesheetRemote
          "https://fonts.googleapis.com/css?family=Play:400,700"
        addStylesheetL
          (StaticR css_bootstrap_3_3_7_min_css)
130
          "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
131 132 133
          [ ( "integrity"
            , "sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u")
          , ("crossorigin", "anonymous")
134 135
          ]
        --   $ StaticR css_bootstrap_css
136 137 138 139 140 141
        $(widgetFile "default-layout")
    withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
  isAuthorized ::
       Route App -- ^ The route the user is visiting.
    -> Bool -- ^ Whether or not this is a "write" request.
    -> Handler AuthResult
142
    -- Routes not requiring authenitcation.
143 144
  isAuthorized FaviconR _ = return Authorized
  isAuthorized RobotsR _  = return Authorized
145
    -- Default to Authorized for now.
146
  isAuthorized _ _        = return Authorized
147 148 149 150
    -- This function creates static content files in the static folder
    -- and names them based on a hash of their content. This allows
    -- expiration dates to be set far in the future without worry of
    -- users receiving stale content.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  addStaticContent ::
       Text -- ^ The file extension
    -> Text -- ^ The MIME content type
    -> LByteString -- ^ The contents of the file
    -> Handler (Maybe (Either Text (Route App, [(Text, Text)])))
  addStaticContent ext mime content = do
    master <- getYesod
    let staticDir = appStaticDir $ appSettings master
    addStaticContentExternal
      minifym
      genFileName
      staticDir
      (StaticR . flip StaticRoute [])
      ext
      mime
      content
167
        -- Generate a unique filename based on the content itself
168 169
    where
      genFileName lbs = "autogen-" ++ base64md5 lbs
170 171
    -- What messages should be logged. The following includes all messages when
    -- in development, and warnings and errors in production.
172 173 174 175 176 177 178
  shouldLogIO :: App -> LogSource -> LogLevel -> IO Bool
  shouldLogIO app _source level =
    return $
    appShouldLogAll (appSettings app) ||
    level == LevelWarn || level == LevelError
  makeLogger :: App -> IO Logger
  makeLogger = return . appLogger
179 180

-- Define breadcrumbs.
181
instance YesodBreadcrumbs App
182 183 184
    -- Takes the route that the user is currently on, and returns a tuple
    -- of the 'Text' that you want the label to display, and a previous
    -- breadcrumb route.
185 186 187 188 189 190
                         where
  breadcrumb ::
       Route App -- ^ The route the user is visiting currently.
    -> Handler (Text, Maybe (Route App))
  breadcrumb HomeR = return ("Home", Nothing)
  breadcrumb _     = return ("home", Nothing)
191 192 193 194

-- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages.
instance RenderMessage App FormMessage where
195 196
  renderMessage :: App -> [Lang] -> FormMessage -> Text
  renderMessage _ _ = defaultFormMessage
197 198 199 200 201

-- Useful when writing code that is re-usable outside of the Handler context.
-- An example is background jobs that send email.
-- This can also be useful for writing code that works across multiple Yesod applications.
instance HasHttpManager App where
202 203
  getHttpManager :: App -> Manager
  getHttpManager = appHttpManager
204 205 206

unsafeHandler :: App -> Handler a -> IO a
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger