packages feed

seacat-1.0.0.0: src/Web/Seacat/RequestHandler.hs

{-# LANGUAGE OverloadedStrings #-}

-- |Building up responses. This module provides a bunch of functions
-- to turn some primitive value into a handler, and the child modules
-- provide more complex handler composition.
--
-- In addition to response building, there are helper functions for
-- accessing the request parameters.
module Web.Seacat.RequestHandler ( -- * Response builders
                                   htmlResponse
                                 , htmlResponse'

                                 , htmlUrlResponse
                                 , htmlUrlResponse'

                                 , textResponse
                                 , textResponse'

                                 , textUrlResponse
                                 , textUrlResponse'

                                 , respond
                                 , respondFile

                                 -- * Redirection
                                 , redirect

                                 -- * Parameter accessors
                                 , param
                                 , param'
                                 , hasParam
                                 , params

                                 -- * File upload handling
                                 , files
                                 , save
                                 , save') where

import Prelude hiding (writeFile)

import Blaze.ByteString.Builder (Builder)
import Blaze.ByteString.Builder.ByteString (fromByteString)
import Control.Applicative ((<$>))
import Control.Monad.IO.Class (liftIO)
import Data.ByteString (unpack)
import Data.ByteString.Lazy (ByteString, writeFile)
import Data.Char (chr)
import Data.Maybe (isJust, fromMaybe)
import Data.Text (Text, pack)
import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Types.Status (Status, ok200, found302)
import Network.Mime (defaultMimeLookup)
import Network.Wai (responseBuilder, responseFile, responseLBS)
import System.Directory (doesFileExist)
import System.FilePath.Posix (joinPath, takeExtension, takeFileName)
import Text.Blaze.Html (Html)
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
import Web.Seacat.Configuration (conf')
import Web.Seacat.RequestHandler.Types

-- |Produce a 200 OK response from the given HTML.
htmlResponse :: PathInfo r => Html -> Handler r
htmlResponse = htmlResponse' ok200

-- |Produce a response from the given HTML and response code.
htmlResponse' :: PathInfo r => Status -> Html -> Handler r
htmlResponse' status html = respond status $ renderHtmlBuilder html

-------------------------

-- |Produce a 200 OK response from the given HTML-generating
-- function.
htmlUrlResponse :: PathInfo r => (MkUrl r -> Html) -> Handler r
htmlUrlResponse = htmlUrlResponse' ok200

-- |Produce a response from the given HTML-generating function and
-- response code.
htmlUrlResponse' :: PathInfo r => Status -> (MkUrl r -> Html) -> Handler r
htmlUrlResponse' status html = do
  mkurl <- askMkUrl
  let builder = renderHtmlBuilder $ html mkurl
  respond status builder

-------------------------

-- |Produce a 200 OK response from the given UTF-8 text.
textResponse :: PathInfo r => Text -> Handler r
textResponse = textResponse' ok200

-- |Produce a response from the given UTF-8 text and response
-- code.
textResponse' :: PathInfo r => Status -> Text -> Handler r
textResponse' status = respond status . fromByteString . encodeUtf8

-------------------------

-- |Produce a 200 OK response from the given UTF-8 text-generating
-- function.
textUrlResponse :: PathInfo r => (MkUrl r -> Text) -> Handler r
textUrlResponse = textUrlResponse' ok200

-- |Produce a response from the given UTF-8 text-generating function
-- and response code.
textUrlResponse' :: PathInfo r => Status -> (MkUrl r -> Text) -> Handler r
textUrlResponse' status text = do
  mkurl <- askMkUrl
  respond status . fromByteString . encodeUtf8 $ text mkurl

-------------------------

-- |Produce a response from the given status and ByteString
-- builder. This sets a content-type of UTF-8 HTML.
respond :: PathInfo r => Status -> Builder -> Handler r
respond status = return . responseBuilder status [("Content-Type", "text/html; charset=utf-8")]

-- |Produce a response from the given file path (minus file
-- root). Call the provided 404 handler if the file isn't found.
respondFile :: PathInfo r => Handler r -> FilePath -> Handler r
respondFile on404 fp = do
  fileroot <- conf' "server" "file_root"
  let fullPath = joinPath [fileroot, fp]

  respondFile' on404 fullPath

-- |Produce a response from the given file path (including any
-- root). Call the provided 404 handler if the file isn't found.
--
-- This is somewhat unsafe as it lets you access files outside the
-- file root, hence why it isn't exported.
respondFile' :: PathInfo r => Handler r -> FilePath -> Handler r
respondFile' on404 fp = do
  exists <- liftIO $ doesFileExist fp
  if exists
  then return $ responseFile ok200 [("Content-Type", defaultMimeLookup $ pack fp)] fp Nothing
  else on404

-------------------------

-- |Produce a response to redirect the user.
redirect :: PathInfo r => r -> Handler r
redirect url = do
  mkurl <- askMkUrl
  return $ responseLBS found302 [("Location", encodeUtf8 $ mkurl url [])] ""

-------------------------

-- |Get a parameter by name. Returns a Maybe Text, where the Text is
-- the value of the parameter, interpreted as a UTF-8 string.
param :: PathInfo r
      => Text -- ^ The name of the parameter
      -> RequestProcessor r (Maybe Text)
param p = lookup p <$> params

-- |Get a parameter with a default value.
param' :: PathInfo r
       => Text -- ^ The parameter name
       -> Text -- ^ The default value
       -> RequestProcessor r Text
param' p d = fromMaybe d <$> lookup p <$> params

-- |Check if a parameter is set
hasParam :: PathInfo r
         => Text -- ^ The parameter name
         -> RequestProcessor r Bool
hasParam p = isJust <$> lookup p <$> params

-- |Get all non-file parameters, with the contents interpreted as
-- UTF-8 strings.
params :: PathInfo r => RequestProcessor r [(Text, Text)]
params = _params <$> askCry

-------------------------

-- |Get all files, stored in memory as a lazy bytestring.
files :: PathInfo r => RequestProcessor r [(Text, FileInfo ByteString)]
files = _files <$> askCry

-- |Save a file to a location relative to the filesystem root,
-- returning the name. File extension is preserved.
save :: PathInfo r => FilePath -> FileInfo ByteString -> RequestProcessor r Text
save fname (FileInfo name _ content) = do
  fileroot <- conf' "server" "file_root"
  let ext    = takeExtension (map (chr . fromIntegral) $ unpack name)
  let path   = joinPath [fileroot, fname]
  let fname' = path ++ ext

  liftIO $ writeFile fname' content

  return . pack $ takeFileName fname'

-- |Save a file to a location relative to the filesystem root, with
-- the path given as segments, returning the name. File extension is
-- preserved.
save' :: PathInfo r => [FilePath] -> FileInfo ByteString -> RequestProcessor r Text
save' = save . joinPath