This repository has been archived on 2024-10-24. You can view files and clone it, but cannot push or open issues or pull requests.
fradrive-old/src/Utils/Print.hs

423 lines
19 KiB
Haskell

-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
module Utils.Print
( renderLetter -- used for generating letter pdfs
, renderLetters
, sendEmailOrLetter -- directly print or sends by email
, printLetter -- always send a letter
, letterApcIdent -- create acknowledge string for APC
, letterFileName -- default filename
, encryptPDF
, sanitizeCmdArg, sanitizeCmdArg', validCmdArgument
-- , compileTemplate, makePDF
, _Meta, addMeta
, toMeta, mbMeta -- single values
, mkMeta, appMeta, applyMetas -- multiple values
, LetterRenewQualificationF(..)
-- , LetterCourseCertificate()
, makeCourseCertificates
) where
-- import Import.NoModel
import Data.Char (isSeparator)
import qualified Data.Text as T
import qualified Data.CaseInsensitive as CI
-- import qualified Data.Foldable as Fold
import qualified Data.ByteString.Lazy as LBS
import Control.Monad.Except
import Import hiding (embedFile)
-- import Data.FileEmbed (embedFile)
import qualified Text.Pandoc as P
-- import qualified Text.Pandoc.PDF as P
-- import qualified Text.Pandoc.Builder as P
import Text.Hamlet
import System.Exit
import System.Process.Typed -- for calling pdftk for pdf encryption
import Handler.Utils.Memcached
import Handler.Utils.Users
import Handler.Utils.DateTime
import Handler.Utils.Mail
import Handler.Utils.Widgets (nameHtml')
import Handler.Utils.Avs (updateReceivers)
import Jobs.Handler.SendNotification.Utils
import Utils.Print.Instances ()
import Utils.Print.Letters
import Utils.Print.RenewQualification
import Utils.Print.CourseCertificate
-- import Model.Types.Markup -- TODO-QSV: should this module be moved accordingly?
{- Recall:
Funktionen außerhalb der Hanlder-Monade gehören in Utils-Module;
ansonsten drohen zyklische Abhängikeiten, d.h.
ggf. Funktionen in der HandlerFor-Monade nach Handler.Utils.Print verschieben!
-}
-------------------------
-- Readers and writers --
-------------------------
-- | Apply StoredMarkup as a template to itself and return the resulting Markup
-- This is a hack to allow variable interpolation within a document.
-- Pandoc currently only allows interpolation within templates.
-- An alternative Route would be to use Builders, but this prevents User-edited Markup Templates
-- reTemplateLetter :: P.PandocMonad m => P.Meta -> StoredMarkup -> m Text
-- reTemplateLetter meta StoredMarkup{..} = do
-- tmpl <- compileTemplate strictMarkupInput
-- doc <- areader readerOpts strictMarkupInput
-- let writerOpts = def { P.writerExtensions = P.pandocExtensions
-- , P.writerTemplate = Just tmpl }
-- P.writeMarkdown writerOpts
-- $ appMeta setIsDeFromLang
-- $ addMeta meta doc
-- where
-- strictMarkupInput = toStrict markupInput
-- readerOpts = def { P.readerExtensions = P.pandocExtensions
-- , P.readerStripComments = True
-- }
-- -- reader :: (P.PandocMonad m, P.ToSources a) => P.ReaderOptions -> a -> m P.Pandoc
-- areader = case markupInputFormat of
-- MarkupHtml -> P.readHtml
-- MarkupMarkdown -> P.readMarkdown
-- MarkupPlaintext -> P.readMarkdown
-- reTemplateLetter' :: P.PandocMonad m => P.Meta -> Text -> m Text
-- reTemplateLetter' meta md = do
-- tmpl <- compileTemplate md
-- doc <- P.readMarkdown readerOpts md
-- let writerOpts = def { P.writerExtensions = P.pandocExtensions
-- , P.writerTemplate = Just tmpl }
-- P.writeMarkdown writerOpts
-- $ appMeta setIsDeFromLang
-- $ addMeta meta doc
-- where
-- readerOpts = def { P.readerExtensions = P.pandocExtensions
-- , P.readerStripComments = True
-- }
-- | read and writes markdown, applying it as its own template to apply meta
mdTemplating :: Text -> P.Meta -> HandlerFor UniWorX (Either Text P.Pandoc)
mdTemplating template meta = runExceptT $ do
let readerOpts = def { P.readerExtensions = P.pandocExtensions
, P.readerStripComments = True
}
-- doc <- ExceptT (pure . over _Left P.renderError . P.runPure $ P.readMarkdown readerOpts template)
-- tmpl <- ExceptT (pure . over _Left P.renderError . P.runPure $ compileTemplate template)
doc <- ExceptT $ memcachedBy (Just . Right $ 6 * diffHour) ("pandoc-md: \n" <> template) (pure . over _Left P.renderError . P.runPure $ P.readMarkdown readerOpts template)
tmpl <- ExceptT $ memcachedBy (Just . Right $ 6 * diffHour) ("template-md: \n" <> template) (pure . over _Left P.renderError . P.runPure $ compileTemplate template)
let writerOpts = def { P.writerExtensions = P.pandocExtensions
, P.writerTemplate = Just tmpl
}
ExceptT . pure . over _Left P.renderError . P.runPure $ do
md_txt <- P.writeMarkdown writerOpts $ appMeta setIsDeFromLang $ addMeta meta doc
P.readMarkdown readerOpts md_txt
-- | creates a PDF using a LaTeX template
pdfLaTeX :: LetterKind -> P.Pandoc -> HandlerFor UniWorX (Either Text LBS.ByteString)
pdfLaTeX lk doc = do
-- e_tmpl <- fmap (over _Left P.renderError) . liftIO . P.runIO $ compileTemplate $ templateLatex lk
e_tmpl <- memcachedBy (Just . Right $ 6 * diffHour) ("LetterKind-latex: \n" <> tshow lk) (fmap (over _Left P.renderError) . liftIO . P.runIO $ compileTemplate $ templateLatex lk)
actRight e_tmpl $ \tmpl -> fmap (over _Left P.renderError) .liftIO . P.runIO $ do
let writerOpts = def { P.writerExtensions = P.pandocExtensions
, P.writerTemplate = Just tmpl }
makePDF writerOpts $ appMeta setIsDeFromLang doc
renderLetter :: (MDLetter l) => Entity User -> l -> Text -> Handler (Either Text LBS.ByteString)
renderLetter rcvrEnt@Entity{entityVal=rcvr} mdl apcIdent = do
now <- liftIO getCurrentTime
formatter@DateTimeFormatter{ format } <- getDateTimeFormatterUser' rcvr
let lang = selectDeEn $ rcvr & userLanguages -- select either German or English only, default de; see Utils.Lang
kind = getLetterKind $ pure mdl
tmpl = getTemplate mdl
meta = addApcIdent apcIdent
<> letterMeta mdl formatter lang rcvrEnt
<> mkMeta
[ -- toMeta "lang" lang -- receiver language is decided in MDLetter instance, since some letters have fixed languages
toMeta "date" $ format SelFormatDate now
, toMeta "rcvr-name" $ rcvr & userDisplayName
, toMeta "address" $ fromMaybe [rcvr & userDisplayName] $ getPostalAddress rcvr
--, toMeta "rcvr-email" $ rcvr & userDisplayEmail -- note that some templates use "email" already otherwise
]
e_md <- mdTemplating tmpl meta
actRight e_md $ pdfLaTeX kind
-- return $ over _Left P.renderError result
-- TODO: apcIdent does not make sense for multiple letters
renderLetters :: (MDLetter l, Foldable f) => Entity User -> f l -> Text -> Handler (Either Text LBS.ByteString)
renderLetters rcvrEnt@Entity{entityVal=rcvr} mdls apcIdent
| Just l <- anyone mdls = do
now <- liftIO getCurrentTime
formatter@DateTimeFormatter{ format } <- getDateTimeFormatterUser' rcvr
let lang = selectDeEn $ rcvr & userLanguages -- select either German or English only, default de; see Utils.Lang
kind = getLetterKind $ pure l
templateCombine _ err@Left{} = pure err
templateCombine mdl (Right doc1) =
let tmpl = getTemplate mdl
meta = addApcIdent apcIdent
<> letterMeta mdl formatter lang rcvrEnt
<> mkMeta
[ -- toMeta "lang" lang -- receiver language is decided in MDLetter instance, since some letters have fixed languages
toMeta "date" $ format SelFormatDate now
, toMeta "rcvr-name" $ rcvr & userDisplayName
, toMeta "address" $ fromMaybe [rcvr & userDisplayName] $ getPostalAddress rcvr
--, toMeta "rcvr-email" $ rcvr & userDisplayEmail -- note that some templates use "email" already otherwise
]
in mdTemplating tmpl meta >>= \case
err@Left{} -> pure err
Right doc2 -> pure $ Right $ doc1 <> doc2
doc <- foldrM templateCombine (Right mempty) mdls
-- result <- actRight doc $ pdfLaTeX kind
-- return $ over _Left P.renderError result
actRight doc $ pdfLaTeX kind
| otherwise = return $ Left "renderLetters received empty set of letters"
---------------
-- PrintJobs --
---------------
-- Only used in print-test-handler for PrintSendR
printLetter :: (MDLetter l) => Maybe UserId -> (Entity User, l) -> Handler (Either Text (Text, FilePath))
printLetter senderId (rcvr, letter) = do
let rcvrId = rcvr ^. _entityKey
encRecipient :: CryptoUUIDUser <- encrypt rcvrId
now <- liftIO getCurrentTime
apcIdent <- letterApcIdent letter encRecipient now
pdf <- renderLetter rcvr letter apcIdent
let protoPji = getPJId letter
pji = protoPji
{ pjiRecipient = Just rcvrId
, pjiSender = senderId
, pjiName = "TEST_" <> pjiName protoPji
, pjiApcAcknowledge = apcIdent
}
actRight pdf $ runDB . printLetter' pji
printLetter' :: PrintJobIdentification -> LBS.ByteString -> DB (Either Text (Text, FilePath))
printLetter' pji pdf = do
let PrintJobIdentification
{ pjiName = printJobName
, pjiApcAcknowledge = printJobApcIdent
, pjiRecipient = printJobRecipient
, pjiSender = printJobSender
, pjiCourse = printJobCourse
, pjiQualification = printJobQualification
, pjiLmsUser = printJobLmsUser
, pjiFileName = fName
} = pji
printJobFilename = T.unpack $ text2asciiAlphaNum fName <> ".pdf"
-- printJobFile <- sinkFileDB True $ yield $ LBS.toStrict pdf -- for PrintJobFile :: FileContentReference use this code
printJobFile = LBS.toStrict pdf
printJobAcknowledged = Nothing
lprPDF printJobFilename pdf >>= \case
Left err -> do
return $ Left err
Right ok -> do
printJobCreated <- liftIO getCurrentTime
-- updateWhere [PrintJobLmsUser ==. printJobLmsUser] [PrintJobLmsUser =. Nothing] -- only one printJob per LmsUser is allowed, since otherwise the qualification table contains double rows
insert_ PrintJob {..}
return $ Right (ok, printJobFilename)
{-
printLetter'' :: _ -> DB PureFile
printLetter'' _ = do
...
return $ File { fileTitle = printJobFilename
, fileModified = printJobCreated
, fileContent = Just $ yield printJobFile
}
-}
sendEmailOrLetter :: (MDLetter l, MDMail l) => UserId -> l -> Handler Bool
sendEmailOrLetter recipient letter = do
(underling, receivers, undercopy) <- updateReceivers recipient -- TODO: check to avoid this almost circular dependency
now <- liftIO getCurrentTime
let pjid = getPJId letter
fName = letterFileName letter
mailSubject = getMailSubject letter -- these are only needed if sent by email, but we're lazy anyway
undername = underling ^. _userDisplayName -- nameHtml' underling
undermail = CI.original $ underling ^. _userEmail
oks <- forM receivers $ \rcvrEnt@Entity{ entityKey = svr, entityVal = rcvrUsr } -> do
encRecipient :: CryptoUUIDUser <- encrypt svr
apcIdent <- letterApcIdent letter encRecipient now
let (preferPost, postal) = getPostalPreferenceAndAddress rcvrUsr
-- mailBody = getMailBody letter formatter
renderLetter rcvrEnt letter apcIdent >>= \case
_ | preferPost, isNothing postal -> do -- neither email nor postal is known
let msg = "Notification failed for " <> tshow encRecipient <> ", who has neither a known email nor postal address. Notification: " <> tshow pjid
$logErrorS "LETTER" msg
return False
Left err -> do -- pdf generation failed
let msg = "Notification failed for " <> tshow encRecipient <> ". PDF generation failed: "<> cropText err <> "For Notification: " <> tshow pjid
$logErrorS "LETTER" msg
return False
Right pdf | preferPost -> -- send printed letter
runDB (printLetter' pjid{pjiRecipient = Just svr, pjiApcAcknowledge = apcIdent} pdf) >>= \case
Left err -> do
let msg = "Notification failed for " <> tshow encRecipient <> ". PDF printing failed. The print job could not be sent: " <> cropText err
$logErrorS "LETTER" msg
return False
Right (msg,_)
| null msg -> return True
| otherwise -> do
$logWarnS "LETTER" $ "PDF printing to send letter with lpr returned ExitSuccess and the following message: " <> msg
return True
Right pdf -> do -- send email
let pdfPass = case encrypPDFfor (pure letter) of
NoPassword -> Nothing
PasswordSupervisor -> rcvrUsr ^. _userPinPassword
PasswordUnderling -> underling ^. _userPinPassword
attachment <- case pdfPass of
Nothing -> return pdf
Just passwd -> encryptPDF passwd pdf >>= \case
Right encPdf -> return encPdf
Left err -> do
let msg = "Notification for " <> tshow encRecipient <> " has unencrypted attachment. Encrypting PDF failed: " <> cropText err
$logWarnS "LETTER" msg
return pdf
formatter <- getDateTimeFormatterUser' rcvrUsr -- not too expensive, only calls getTimeLocale
let isSupervised = recipient /= svr
supername = rcvrUsr ^. _userDisplayName -- nameHtml' rcvrUsr
mailBody = getMailBody letter formatter
userMailTdirect svr $ do
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI mailSubject
editNotifications <- mkEditNotifications svr
addHtmlMarkdownAlternatives $(ihamletFile "templates/mail/genericMailLetter.hamlet")
addPart (File { fileTitle = fName
, fileModified = now
, fileContent = Just $ yield $ LBS.toStrict attachment
} :: PureFile)
return True
return $ or oks
-----------------------------
-- Typed Process Utilities --
-----------------------------
-- | Converts Triple consisting of @ExitCode@, Success- and Failure-Value to Either Failue- or Success-Value.
-- Returns @Right@ if the @ExitCode@ is @ExitsSuccess, entirely ignoring the Failure-Value, which might contain warning messages.
-- To be used with 'System.Process.Typed.readProcess'
exit2either :: Monoid a => (ExitCode, a, a) -> Either a a
exit2either (ExitSuccess , stdOut, errOut) = Right $ stdOut <> errOut
exit2either (ExitFailure _ , stdOut, errOut) = Left $ stdOut <> errOut
readProcess' :: MonadIO m => ProcessConfig stdin stdoutIgnored stderrIgnored -> m (ExitCode, Text, Text)
readProcess' pc = do
(ec, bs_err, bs_out) <- readProcess pc
let st_err = decodeUtf8 $ LBS.toStrict bs_err
st_out = decodeUtf8 $ LBS.toStrict bs_out
return (ec, st_err, st_out)
sanitizeCmdArg :: Text -> Text
sanitizeCmdArg = T.filter (\c -> c /= '\'' && c /= '"' && c/= '\\' && not (isSeparator c))
sanitizeCmdArg' :: String -> String
sanitizeCmdArg' = filter (\c -> c /= '\'' && c /= '"' && c/= '\\' && not (isSeparator c))
-- | Returns Nothing if ok, otherwise the first mismatching character
-- Pin Password is used as a commandline argument in Utils.Print.encryptPDF and hence poses a security risk
validCmdArgument :: Text -> Maybe Char
validCmdArgument t = t `textDiff` sanitizeCmdArg t
-----------
-- pdftk --
-----------
--
-- We use the external tool pdftk for PDF encryption like so
-- > pdftk in.pdf output out.pdf user_pw tomatenmarmelade
-- we can use stdin and std out like so
-- > pdftk - output - user_pw tomatenmarmelade
--
encryptPDF :: MonadIO m => Text -> LBS.ByteString -> m (Either Text LBS.ByteString)
encryptPDF pw bs = over _Left (decodeUtf8 . LBS.toStrict) . exit2either <$> readProcess pc
where
pw' = sanitizeCmdArg pw
pc = setStdin (byteStringInput bs) $
proc "pdftk" [ "-" -- read from stdin
, "output", "-" -- write to stdout
, "user_pw", T.unpack $ T.strip pw' -- encrypt pdf content
, "dont_ask" -- no interaction
, "allow", "Printing" -- allow printing despite encryption
]
-- Note that pdftk will issue a warning, which will be ignored:
-- Warning: Using a password on the command line interface can be insecure.
-- Use the keyword PROMPT to supply a password via standard input instead.
---------
-- lpr --
---------
--
-- We use the external tool lpr in the variant supplied by busybox
-- to print pdfs like so:
-- > lpr -P fradrive@fravm017173.fra.fraport.de:515 -J printJobName -
--
-- The cups version of lpr is instead used like so:
-- > lpr -P fradrive -H fravm017173.fra.fraport.de:515 -T printJobName -
-- | Internal only, use `printLetter` instead
lprPDF :: (MonadHandler m, HasAppSettings (HandlerSite m)) => FilePath -> LBS.ByteString -> m (Either Text Text)
lprPDF (sanitizeCmdArg' -> jb) bs = do
mbLprServerArg <- getLprServerArg
case mbLprServerArg of
Nothing -> return $ Right "Print command ignored due to setting 'mail-reroute-to' being set."
Just lprServerArg -> do
let pc = setStdin (byteStringInput bs) $
proc "lpr" $
jobname ++ -- -J jobname -- a name for job identification at printing site
[ lprServerArg -- -P queue@hostname:port
, "-" -- read from stdin
]
jobname | null jb = []
| otherwise = ["-J " <> jb]
exit2either <$> readProcess' pc
where
getLprServerArg = do
rerouteMail <- getsYesod $ view _appMailRerouteTo
case rerouteMail of
Just _ -> return Nothing
Nothing -> do
LprConf{..} <- getsYesod $ view _appLprConf
return . Just $ "-P " <> lprQueue <> "@" <> lprHost <> ":" <> show lprPort
{- -- Variant without caching
lprPDF' :: (MonadHandler m, HasAppSettings (HandlerSite m)) => String -> LBS.ByteString -> m (Either Text Text)
lprPDF' jb bs = do
LprConf{..} <- getsYesod $ view _appLprConf
let lprServer = lprQueue <> "@" <> lprHost <> ":" <> show lprPort
pc = setStdin (byteStringInput bs) $
proc "lpr" $ [ "-P " <> lprServer -- queue@hostname:port
] ++ jobname ++ -- a name for job identification at printing site
[ "-" -- read from stdin
]
jobname | null jb = []
| otherwise = ["-J " <> jb]
exit2either <$> readProcess' pc
-}