darcsden-1.1: src/DarcsDen/WebUtils.hs
{-# LANGUAGE OverloadedStrings #-}
module DarcsDen.WebUtils where
import Control.Exception
import Control.Monad.Trans
import Data.List (find)
import Data.Maybe (isNothing)
import Data.Monoid (mconcat)
import HSP (XML, evalHSP, renderXML, renderAsHTML)
import Snap.Core
import Snap.Util.FileServe
import System.FilePath
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8 (pack)
import qualified Data.Map as M
import DarcsDen.Pages.HSPage ( HSPage )
import DarcsDen.State.Session
import DarcsDen.State.Repository (getRepository)
import DarcsDen.State.Util (repoDir)
import DarcsDen.Util (fromBS, toBS)
type Page = Session -> Snap ()
notFound :: Snap ()
notFound = notFoundPage ""
notFoundPage :: BS.ByteString -> Snap ()
notFoundPage msg = do
putResponse
$ setResponseStatus 404 "Not Found"
$ setContentType "text/html; charset=utf-8"
emptyResponse
r <- getRequest
writeBS $ errorPageLayout r "background-color:#ffe; color:orange;" "not found" msg
withResponse finishWith
tobs :: String -> BS.ByteString
tobs = B8.pack
exceptionPage :: SomeException -> Snap ()
exceptionPage e = do
r <- getRequest
errorPage' $ errorPageLayout r "background-color:#fee; color:red;" "error" (tobs $ show e)
errorPage :: BS.ByteString -> Snap ()
errorPage msg = do
r <- getRequest
errorPage' $ errorPageLayout r "background-color:#fee; color:red;" "error" msg
errorPageLayout :: Request -> BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString
errorPageLayout req style title content =
BS.intercalate "\n" $
["<html>"
,"<body style='text-align:center;'>"
,mconcat ["<div style='margin:auto; display:inline-block; padding:1em 3em;",style,"'>"]
,mconcat ["<h1 style='font-family:sans-serif; font-size:4em; margin:0;'>",title,"</h1>"]
,"<p style='font-family:sans-serif; font-size:1em;'>"
,mconcat ["Sorry.. your ", B8.pack $ show $ rqMethod req, " request to ", rqURI req, " failed. The details have been logged."]
,"</p>"
,"<pre style='font-family:sans-serif; font-size:1em;'>"
,humaniseError content
,"</pre>"
,"</div>"
,"</body>"
,"</html>"
]
humaniseError :: BS.ByteString -> BS.ByteString
humaniseError msg
-- the most common cause of errors
| match "thread killed"
|| (match "user error (internal error: server error: GET http://127.0.0.1:5984"
&& match "users/_view/by_email"
&& match "Content-Length: 0") =
-- "That strange couchdb users by email error happened. We don't know the cause yet."
-- "This request took too long and timed out. We need someone to add caching and background jobs to <a href=\"http://hub.darcs.net/simon/darcsden/browse/src/DarcsDen/Handler/Repository/Browse.hs\">darcsden</a>."
"This request took too long and timed out."
| match "failed to read patch" =
"This request took too long and timed out, or there was a problem reading this patch."
| match "CouchDB" || match "http://127.0.0.1:5984" =
"There was a problem querying the database."
| match "ExceptionAlreadyCaught" =
"ExceptionAlreadyCaught. Hmm, that's unusual."
| otherwise = msg
where match = (`BS.isInfixOf` msg)
errorPage' :: BS.ByteString -> Snap ()
errorPage' html = do
putResponse
$ setResponseStatus 500 "Internal Server Error"
$ setContentType "text/html; charset=utf-8"
emptyResponse
writeBS html
withResponse finishWith
-- finishWith
-- $ setResponseStatus 500 "Internal Server Error"
-- . setContentType "text/html; charset=utf-8"
-- . setContentLength (fromIntegral $ B.length p)
-- . modifyResponseBody (>==> enumBuilder (fromByteString p))
-- $ emptyResponse
-- import Snap.Iteratee ((>==>), enumBuilder)
redirectTo :: String -> Snap ()
redirectTo dest = do
putResponse (setResponseStatus 302 "Found" emptyResponse)
withResponse (finishWith . addHeader "Location" (toBS dest))
withSession :: Page -> Snap ()
withSession p = do
r <- getRequest
case find ((== "DarcsDenSession") . cookieName) (rqCookies r) of
Nothing -> withNewSession p
Just (Cookie { cookieValue = sid }) -> do
ms <- getSession sid
case ms of
Just s -> p s
Nothing -> withNewSession p
withNewSession :: Page -> Snap ()
withNewSession r = do
ms <- newSession
case ms of
Just s -> do
modifyResponse $ addResponseCookie
Cookie { cookieName = "DarcsDenSession"
, cookieValue = sID s
, cookieExpires = Just (sExpire s)
, cookieDomain = Nothing
, cookiePath = Just "/"
, cookieSecure = False
, cookieHttpOnly = False
}
r s
Nothing -> errorPage "Session could not be created."
repoServe :: String -> Snap ()
repoServe b = do
mo <- getParam "user"
mr <- getParam "repo"
case (mo, mr) of
(Just owner, Just repo) -> do
private <- fmap isNothing (getRepository (fromBS owner, fromBS repo))
if private
then notFound
else serveDirectory (repoDir (fromBS owner) (fromBS repo) </> b)
_ ->
notFound
input :: String -> String -> Snap String
input p d = do
param <- getParam (toBS p)
return (maybe d fromBS param)
getInputs :: Snap [(String, String)]
getInputs = fmap
(map (\(k, vs) -> (fromBS k, vs >>= fromBS)) . M.toList)
(withRequest (return . rqParams))
-- Page helpers
doPage' :: (XML -> String) -> BS.ByteString -> HSPage -> Page
doPage' render contentType p s = do
(_, page) <- liftIO $ evalHSP Nothing (p s)
modifyResponse (addHeader "Content-Type" contentType)
writeBS (toBS $ render page)
clearNotifications s
doPage :: HSPage -> Page
doPage = doPage' (("<!DOCTYPE html>\n" ++) . renderAsHTML) "text/html; charset=utf-8"
doAtomPage :: HSPage -> Page
doAtomPage = doPage' (("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" ++) . renderXML) "application/atom+xml; charset=utf-8"