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