WashNGo-2.12: WASH/CGI/BaseCombinators.hs
-- © 2001-2005 Peter Thiemann
module WASH.CGI.BaseCombinators
{-- interface
(ask -- WithHTML x CGI a -> CGI ()
,tell -- CGIOutput a => a -> CGI ()
,htell -- WithHTML x IO () -> CGI ()
,run -- CGI () -> IO ()
,runWithHook -- ([String] -> CGI ()) -> CGI () -> IO ()
)
--}
where
import qualified List
import Maybe
import Monad
import System
import IO
import WASH.Utility.Auxiliary
import qualified WASH.Utility.Base64 as Base64
import WASH.CGI.CookieIO
import WASH.CGI.CGIMonad
import WASH.CGI.CGIOutput
import WASH.CGI.CGITypes
import WASH.CGI.EventHandlers
import WASH.CGI.Fields
import WASH.CGI.Frames
import WASH.CGI.LogEntry
import WASH.CGI.CGIHistory
import qualified WASH.CGI.HTMLWrapper as H hiding (map,head)
import WASH.CGI.Images
import WASH.Utility.JavaScript
import qualified WASH.Utility.RFC2279 as RFC2279
import qualified WASH.Utility.RFC2397 as RFC2397
import WASH.CGI.RawCGIInternal hiding (CGIEnv (..), getSessionMode)
import qualified WASH.Utility.URLCoding as URLCoding
import WASH.CGI.CGIConfig
-- |Safe embedding of an 'IO' action into the 'CGI' monad. Intentionally not
-- parameterized óver its monad to avoid its use inside of transactions.
io :: (Read a, Show a) => IO a -> CGI a
io ioa =
once (unsafe_io ioa)
-- |Runs a 'CGI' state transformer forever. Its specification is just
--
-- > feedback f x = f x >>= feedback f
--
-- However, 'feedback' is more efficient because it avoids the
-- buildup of long interaction logs by cutting back every time just before 'f'
-- is invoked. Unfortunately, it's useless due to typing restrictions.
feedback :: (CGIMonad cgi, Read a, Show a) => (a -> cgi a) -> a -> cgi a
feedback f x =
wrapCGI (\ cgistate ->
case inparm cgistate of
-- if we've got a value in the log,
-- ignore the parameter x and just use the logged value
PAR_MARK _ : PAR_RESULT code : rest ->
case reads code of
(result, ""):_ ->
return (result
,cgistate { inparm = rest })
_ ->
reportError "Result unreadable"
(H.text_S "Cannot read " >> H.text (show code)) cgistate
[] ->
let stid = stateID cgistate
newparm = PAR_MARK stid
newvalue = PAR_RESULT (show x)
in
do (nextx, cgistate') <-
unwrapCGI (f x) cgistate { outparm = newvalue : newparm : outparm cgistate
, stateID = nextstid (nextstid stid newparm) newvalue }
let (oldstid, outparm') = popToMark $ outparm cgistate'
unwrapCGI (feedback f nextx)
cgistate { inparm = inparm cgistate'
, outparm = outparm'
, stateID = oldstid
, cookieMap = cookieMap cgistate'
, cookiesToSend = cookiesToSend cgistate' }
_ ->
reportError "Out of sync" H.empty cgistate)
-- |Control operator for the 'CGI' monad. Its specification is
--
-- > callWithCurrentHistory (\backto x -> action x >>= backto) x
-- > ==
-- > action x >>= callWithCurrentHistory (\backto x -> action x >>= backto)
--
-- However, 'callWithCurrentHistory' is more efficient because it avoids the
-- buildup of long interaction logs by cutting back every time just before
-- 'action' gets executed.
callWithCurrentHistory :: (CGIMonad cgi, Read a, Show a) =>
((a -> cgi ()) -> a -> cgi ()) -> a -> cgi ()
callWithCurrentHistory g x =
wrapCGI (\ cgistate ->
let
prompt stid x =
wrapCGI (\ cgistate' ->
let outparm' = popTo stid (outparm cgistate')
in
unwrapCGI (callWithCurrentHistory g x)
cgistate { inparm = inparm cgistate'
, outparm = outparm'
, stateID = stid
, cookieMap = cookieMap cgistate'
, cookiesToSend = cookiesToSend cgistate' } )
popTo stid [] = error "popTo did not find its mark"
popTo stid (PAR_MARK stid' : rest) | stid' == stid = rest
popTo stid (_ : rest) = popTo stid rest
in
case inparm cgistate of
-- if we've got a value in the log,
-- ignore the parameter x and just use the logged value
PAR_MARK stid : PAR_RESULT code : rest ->
case reads code of
(result, ""):_ ->
unwrapCGI (g (prompt stid) result) cgistate { inparm = rest }
_ ->
reportError "Result unreadable"
(H.text_S "Cannot read " >> H.text (show code)) cgistate
[] ->
let stid = stateID cgistate
newmark = PAR_MARK stid
newvalue = PAR_RESULT (show x)
in
unwrapCGI (g (prompt stid) x)
cgistate { outparm = newvalue : newmark : outparm cgistate
, stateID = nextstid (nextstid stid newmark) newvalue }
_ ->
reportError "Out of sync" H.empty cgistate)
-- |Brackets a 'CGI' action so that only its result is visible. Improves
-- efficiency by not executing the bracketed action after it has been performed
-- once. Use this for avoiding the inefficient buildup of long interaction logs.
once :: (CGIMonad cgi, Read a, Show a) => cgi a -> cgi a
once cgi =
wrapCGI (\ cgistate ->
case inparm cgistate of
PAR_RESULT code : rest ->
case reads code of
(result, ""):_ ->
return (result
,cgistate { inparm = rest })
_ ->
reportError "Result unreadable"
(H.text_S "Cannot read " >> H.text (show code)) cgistate
[] ->
let stid = stateID cgistate
newparm = PAR_MARK stid in
unwrapCGI cgi cgistate { outparm = newparm : outparm cgistate
, stateID = nextstid stid newparm }
>>= finish cgistate
PAR_MARK _ : rest ->
unwrapCGI cgi cgistate { inparm = rest } >>= finish cgistate
_ ->
reportError "Out of sync" H.empty cgistate)
where finish cgistate (v, cgistate') =
let (oldstid, outparm') = popToMark $ outparm cgistate'
newparm = PAR_RESULT (show v)
in
return (v
,cgistate { inparm = inparm cgistate'
, outparm = newparm : outparm'
, stateID = nextstid oldstid newparm
, cookieMap = cookieMap cgistate'
, cookiesToSend = cookiesToSend cgistate' })
-- |Repeats a 'CGI' action without saving its state so that the size of the
-- interaction log remains constant.
forever :: (CGIMonad cgi) => cgi () -> cgi ()
forever cgi =
wrapCGI (\ cgistate ->
case inparm cgistate of
[] ->
let stid = stateID cgistate
newparm = PAR_MARK stid in
unwrapCGI cgi cgistate { outparm = newparm : outparm cgistate
, stateID = nextstid stid newparm }
>>=
const (reportError "Black hole" H.empty cgistate)
PAR_MARK _ : rest ->
unwrapCGI cgi cgistate { inparm = rest } >>= finish (mcount cgistate)
_ ->
reportError "Out of sync" H.empty cgistate)
where finish previousMcount (v, cgistate') =
let (oldstid, outparm') = popToMark $ outparm cgistate'
newparm_ignored = PAR_RESULT (show v)
newparm = PAR_MARK oldstid
in
unwrapCGI cgi
cgistate' { inparm = []
, outparm = newparm : outparm'
, stateID = nextstid oldstid newparm
, mcount = previousMcount }
-- |Unsafe variant of 'once': returns the computed value only the first time
-- and returns a default value in all later invocations.
-- [deprecated]
onceAndThen :: (CGIMonad cgi, Read a, Show a) => a -> cgi a -> cgi a
onceAndThen a cgi =
wrapCGI (\ cgistate ->
case inparm cgistate of
PAR_IGNORED : rest ->
return (a
,cgistate { inparm = rest })
[] ->
unwrapCGI cgi cgistate { outparm = PAR_MARK (stateID cgistate) : outparm cgistate }
>>= finish
PAR_MARK _ : rest ->
unwrapCGI cgi cgistate { inparm = rest } >>= finish
_ ->
reportError "Out of sync" H.empty cgistate)
where popToMark [] = []
popToMark (PAR_MARK _: rest) = rest
popToMark (_: rest) = popToMark rest
finish (v, cgistate') =
let out = outparm cgistate' in
return (v
,cgistate' { outparm = PAR_IGNORED : popToMark out })
-- internal helper function
popToMark [] = error "popToMark did not find MARK"
popToMark (PAR_MARK v: rest) = (v, rest)
popToMark (_: rest) = popToMark rest
-- |Directly lifts the 'IO' monad into the 'CGI' monad. This is generally unsafe
-- and should be avoided. Use 'io' instead.
unsafe_io :: IO a -> CGI a
unsafe_io = lift
-- |Takes a monadic value that constructs a HTML page and delivers this
-- page to the browser. This page may contain forms and input widgets.
ask :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
ask ma =
do sessionMode <- getSessionMode
case sessionMode of
LogOnly ->
askResumptive ma
_ ->
askContinuously ma
-- |Implementation of `ask` for the pure logged variant.
askResumptive :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
askResumptive ma =
do wrapCGI (\cgistate ->
return ((), cgistate { pageInfo = initialPageInfo cgistate }))
setAction tell
elem <- H.build_document ma
wrapCGI $
\cgistate -> let
pi = pageInfo cgistate
atable = actionTable pi
mbnds = bindings pi
msubmitter = mbnds >>= assocParm subVar
maction = msubmitter >>= \x -> lookup x atable
nextState = nextCGIState cgistate
defsubmission = liftM snd $ listToMaybe (reverse atable)
defaction = maybe (rawTell)
(const (maybe (tellError "Unspecified action")
id
defsubmission))
mbnds
go = (maybe defaction id maction) elem nextState
oldgo = (nextaction pi elem) nextState
in
-- appendDebugFile "/tmp/ask" (show (List.map fst atable, msubmitter)) >>
go
-- |Experimental implementation of continuous version.
askContinuously :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
askContinuously ma =
fromCGIstate id >>= \initialState ->
let
scriptName = cgiScriptName (cgiInfo initialState)
parentID = (scriptName, stateID initialState)
askLoop = do
wrapCGI (\cgistate ->
return ((), cgistate { pageInfo = initialPageInfo cgistate }))
setAction tellContinuously -- is this still required?
elem <- H.build_document ma
wrapCGI $
\cgistate -> let
tellAction x = \ cst ->
do rawTellContinuously x cst
hClose (cgiHandle (cgiInfo cgistate))
(decoded_parameters, hout) <- readParameters parentID
-- newparm :: CGIParameters
let newparm = PAR_VALUES $ dropSpecialParameters decoded_parameters
newState = initialState { inparm = [newparm]
, outparm = newparm : outparm initialState
, cgiInfo = (cgiInfo initialState) { cgiHandle = hout}
}
unwrapCGI askLoop newState
pi = pageInfo cgistate
atable = actionTable pi
mbnds = bindings pi
msubmitter = mbnds >>= assocParm subVar
maction = msubmitter >>= \x -> lookup x atable
nextState = nextCGIState cgistate
defsubmission = liftM snd $ listToMaybe (reverse atable)
nextparam = head (inparm cgistate)
myID = (scriptName, nextstid (snd parentID) nextparam)
boost maction =
maction >>= \ cgiact ->
return (\ elem ->
unwrapCGI (lift (createEntry myID (Just parentID) nextparam Nothing)
>> wrapCGI (cgiact elem)))
defaction = maybe tellAction
(const (maybe (tellError "Unspecified action")
id
(boost defsubmission)))
mbnds
in
(maybe defaction id (boost maction)) elem nextState
in askLoop
-- |Like 'ask', but passes the constructed HTML page to the @elementAction@
-- parameter. This function may send the page via Email or store it into a
-- file. Anyone loading this page in a browser can resume the interaction.
askOffline :: (CGIMonad cgi) => H.WithHTML x cgi a -> (H.Element -> IO ()) -> cgi ()
askOffline ma elementAction =
do wrapCGI (\cgistate -> return ((), cgistate { pageInfo = initialPageInfo cgistate }))
setAction tell
elem <- H.build_document ma
wrapCGI $
\cgistate ->
case bindings (pageInfo cgistate) of
Nothing ->
elementAction elem >> return ((), cgistate)
Just _ ->
(nextaction (pageInfo cgistate) elem) (nextCGIState cgistate)
-- |Turns a 'CGI' action into an 'IO' action. Used to turn the main 'CGI' action
-- into the @main@ function of the program. Typical use:
--
-- > main = run mainCGI
run :: CGI () -> IO ()
run =
runWithOptions []
-- |Turns a 'CGI' action into an 'IO' action. Used to turn the main 'CGI' action
-- into the @main@ function of the program. Takes additional low-level
-- options. Typical use:
--
-- > main = runWithOptions [] mainCGI
runWithOptions :: CGIOptions -> CGI () -> IO ()
runWithOptions options =
runInternal options (fallbackTranslator Nothing)
-- |Variant of 'run' where an additional argument @cgigen@ specifies an action
-- taken when the script is invoked with a non-empty query string as in
-- @script-name?query-string@
runWithHook :: CGIOptions -> ([String] -> CGI ()) -> CGI () -> IO ()
runWithHook options cgigen =
runInternal options (fallbackTranslator (Just cgigen))
runInternal options cgigen cgiProg =
start options $ makeServletInternal cgigen cgiProg
-- |Transform a CGI action into a servlet suitable for running from Marlow's web
-- server.
makeServlet :: CGI () -> CGIProgram
makeServlet cgiProg =
makeServletInternal (fallbackTranslator Nothing) cgiProg
-- |Like 'makeServlet' with additional CGI generator as in 'runWithHook'.
makeServletWithHook :: ([String] -> CGI ()) -> CGI () -> CGIProgram
makeServletWithHook cgigen cgiProg =
makeServletInternal (fallbackTranslator $ Just cgigen) cgiProg
makeServletInternal cgigen (CGI cgi) = \ info decoded_parameters ->
let maybecgiparm = assocParm "=CGI=parm=" decoded_parameters in
let maybecgistid = assocParm "=CGI=stid=" decoded_parameters in
let maybejsenabl = assocParm "js_enabled" decoded_parameters in
let clean_parameters = dropSpecialParameters decoded_parameters in
let no_parameters = null decoded_parameters in
do oldparm <- case maybecgiparm of
Just cgiparm ->
liftM read $ liftM RFC2279.decode $ decode $ Base64.decode' $ cgiparm
Nothing ->
return []
key <- generateKey
let newparm
| no_parameters = []
| otherwise = (PAR_VALUES $ clean_parameters)
: oldparm
oldstid
| null decoded_parameters || isNothing maybecgistid = initialStateID
| otherwise = Base64.decode' $ fromJust maybecgistid
cgistate = CGIState { inparm = reverse newparm
, outparm = newparm
, stateID = oldstid
, cgiInfo = info
, mcount = 0
, jsEnabled = liftM read maybejsenabl
, pageInfo = (initialPageInfo cgistate) {inFrame = 0}
, encoder = makeEncoder key
, cookieMap = map decodeCookie (cgiCookies info)
, cookiesToSend = []
}
args = cgiArgs info
scriptName = cgiScriptName info
sessionMode = cgiSessionMode info
-- writeDebugFile "/tmp/CGIOLDPARM" (show oldparm)
-- writeDebugFile "/tmp/CGINEWPARM" (show newparm)
-- set b==False if replaying is required
b <- case sessionMode of
LogOnly ->
return False
_ ->
if no_parameters then
do createEntry (scriptName, initialStateID) Nothing PAR_IGNORED Nothing
return False
else
notify (scriptName, oldstid) clean_parameters (cgiHandle info)
-- if b is True at this point, we have found a thread to handle
-- this request. However, we cannot terminate at this time because
-- exiting will close the output handle for the other thread!
unless b $ do
if null args || null (head args)
then cgi cgistate >> return ()
else unCGI (cgigen $ args) cgistate >> return ()
exitWith ExitSuccess
-- ======================================================================
-- output routines
-- |Terminates script by sending its argument to the browser.
tell :: (CGIMonad cgi, CGIOutput a) => a -> cgi ()
tell a =
wrapCGI (rawTell a)
-- rawTell :: (CGIOutput a) => a -> CGIAction ()
rawTell a cgistate =
-- appendFile "/tmp/CGIFRAME" ("tell/enter\n") >>
let frameno = inFrame $ pageInfo cgistate in
-- appendFile "/tmp/CGIFRAME" ("tell #" ++ show frameno ++ "\n") >>
if frameno == 0 then
do putCookies cgistate
cgiPut (cgiHandle $ cgiInfo cgistate) a
exitWith ExitSuccess
else
do let fname = frameFullPath (outparm cgistate) frameno
h <- openFile fname WriteMode
cgiPut h a
hClose h
return ((), cgistate)
-- |(experimental: continuous version) send argument to the browser.
tellContinuously :: (CGIMonad cgi, CGIOutput a) => a -> cgi ()
tellContinuously a =
wrapCGI (rawTellContinuously a)
rawTellContinuously :: (CGIOutput a) => a -> CGIAction ()
rawTellContinuously a =
(\cgistate ->
let h = cgiHandle $ cgiInfo cgistate in
do putCookies cgistate
cgiPut h a
-- hFlush h
return ((), cgistate))
-- |Terminate script by sending a HTML page constructed by monadic argument.
htell :: (CGIMonad cgi) => H.WithHTML x IO () -> cgi a
htell hma =
wrapCGI (\cgistate ->
do putCookies cgistate
itell (cgiHandle $ cgiInfo cgistate) hma)
-- never reached
rawHtell hma =
\cgistate ->
do putCookies cgistate
itell (cgiHandle $ cgiInfo cgistate) hma
--
tellError :: String -> H.Element -> CGIAction a
tellError str elems =
rawHtell message
where message =
H.standardPage str (backLink H.empty)
--
reportError :: String -> H.WithHTML x IO () -> CGIState -> IO (a, CGIState)
reportError ttl elems cgistate =
unCGI (htell message) cgistate
-- never reached
where message =
H.standardPage ttl (elems >> backLink H.empty)
-- |Link to previous page in browser's history. Uses JavaScript.
backLink :: Monad m => H.HTMLCons x y m ()
backLink attrs =
hlink (URL "javascript:back()") (H.text_S "Try again..." >> attrs)
-- |Plain Hyperlink from an URL string.
hlink :: Monad m => URL -> H.HTMLCons x y m ()
hlink url subs =
H.a_T (H.attr_SD "href" (unURL url) >> subs)
--
fallbackTranslator mCgigen =
docTranslator [nothing, question] $
frameTranslator $
nextTranslator mCgigen
--
frameTranslator :: (CGIMonad cgi) => ([String] -> cgi ()) -> [String] -> cgi ()
frameTranslator next (name@('F':'R':'A':'M':'E':':':_):_) =
tell (ResponseFileReference (frameDir ++ name))
frameTranslator next strs =
next strs
-- |A /translator/ is a function @[String] -> CGI ()@. It takes the query string
-- of the URL (of type @[String]@) and translates it into a CGI
-- action. @docTranslator docs next@
-- takes a list of 'FreeForm' documents and a next translator. It tries to
-- select a document by its 'ffName' and falls through to the
-- @next@ translator if no document matches.
docTranslator :: [FreeForm] -> ([String] -> CGI ()) -> [String] -> CGI ()
docTranslator docs next [name] =
let f (doc : rest) =
if name == ffName doc then tell doc else f rest
f [] = next [name]
in f docs
docTranslator docs next strs =
next strs
-- |Terminates a sequence of translators.
lastTranslator :: [String] -> CGI ()
lastTranslator =
nextTranslator Nothing
nextTranslator Nothing _ =
tell (Status 404 "Not Found" Nothing)
nextTranslator (Just cgigen) strs =
cgigen strs
-- | Internal: name for the submission variable and form field
subVar :: String
subVar = "WASHsub"