{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-}
module Text.ICalendar.Parser.Properties where
import Control.Applicative
import Control.Monad.Error hiding (mapM)
import Control.Monad.RWS (asks)
import qualified Data.ByteString.Base64.Lazy as B64
import qualified Data.ByteString.Lazy.Char8 as B
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.Char
import Data.Default
import Data.Maybe
import qualified Data.Set as S
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import Data.Traversable (mapM)
import qualified Data.Version as Ver
import Prelude hiding (mapM)
import Text.ParserCombinators.ReadP (readP_to_S)
import Text.Parsec.Prim hiding ((<|>))
import Text.ICalendar.Parser.Common
import Text.ICalendar.Parser.Parameters
import Text.ICalendar.Types
parseFreeBusy :: Content -> ContentParser FreeBusy
parseFreeBusy (ContentLine _ "FREEBUSY" o bs) = do
typ <- maybe (return def) (parseFBType . CI.mk .: paramOnlyOne) $
lookup "FBTYPE" o
periods <- S.fromList .: mapM parseUTCPeriod $ B.split ',' bs
return $ FreeBusy typ periods (toO $ filter ((/="FBTYPE").fst) o)
parseFreeBusy x = throwError $ "parseFreeBusy: " ++ show x
parseXDurationOpt :: CI Text
-> (DateTime -> OtherParams -> a)
-> (Date -> OtherParams -> a)
-> Maybe DTStart
-> ContentParser (Maybe (Either a DurationProp))
parseXDurationOpt w a b dts = do
dte <- optLine1 w $ Just .: parseSimpleDateOrDateTime a b
dur <- optLine1 "DURATION" $ Just .: parseDurationProp dts
case (dte, dur) of
(Nothing, Nothing) -> return Nothing
(Just x, Nothing) -> return . Just $ Left x
(Nothing, Just x) -> return . Just $ Right x
_ -> throwError "Either DTEND or DURATION can be specified, but not \
\both."
parseTrigger :: Content -> ContentParser Trigger
parseTrigger (ContentLine _ "TRIGGER" o bs) = do
value <- paramOnlyOne . fromMaybe ["DURATION"] $ lookup "VALUE" o
case value of
"DURATION" -> do
rel <- maybe (return def)
(parseAlarmTriggerRelationship . CI.mk
<=< paramOnlyOne) $ lookup "RELATED" o
let o' = filter (\(x,_) -> x /= "VALUE" && x /= "RELATED") o
val <- parseDuration "TRIGGER" bs
return $ TriggerDuration val rel (toO o')
"DATE-TIME" -> do val <- mustBeUTC =<< parseDateTime Nothing bs
let o' = filter (\(x, _) -> x /= "VALUE") o
return $ TriggerDateTime val (toO o')
x -> throwError $ "parseTrigger: invalid value: " ++ show x
parseTrigger x = throwError $ "parseTrigger: " ++ show x
parseRelatedTo :: Content -> ContentParser RelatedTo
parseRelatedTo (ContentLine _ "RELATED-TO" o bs) = do
val <- valueOnlyOne =<< parseText bs
typ <- maybe (return def) (parseRelationshipType . CI.mk .: paramOnlyOne) $
lookup "RELTYPE" o
return $ RelatedTo val typ (toO $ filter (\(x,_) -> x /= "RELTYPE") o)
parseRelatedTo x = throwError $ "parseRelatedTo: " ++ show x
parseRequestStatus :: Content -> ContentParser RequestStatus
parseRequestStatus (ContentLine _ "REQUEST-STATUS" o bs) = do
let (statcode', rest) = B.break (==';') bs
statcode :: Maybe [Int]
statcode = mapM (maybeRead . B.unpack) $ B.split '.' statcode'
when (isNothing statcode) .
throwError $ "parseRequestStatus: invalid code: " ++ show bs
when (B.null rest) .
throwError $ "parseRequestStatus: missing statdesc: " ++ show bs
(statdesc, rest') <- (\(a,b) -> (,b) <$> valueOnlyOne a)
<=< parseText' $ B.tail rest
statext <- if B.null rest'
then return Nothing
else do when (B.head rest' /= ';') .
throwError $ "parseRequestStatus: bad desc: " ++
show bs
Just <$> (valueOnlyOne =<< parseText (B.tail rest'))
lang <- mapM paramOnlyOne $ Language . CI.mk .: lookup "LANGUAGE" o
let o' = filter (\(x, _) -> x `notElem` ["LANGUAGE"]) o
return $ RequestStatus (fromJust statcode) statdesc lang statext (toO o')
parseRequestStatus x = throwError $ "parseRequestStatus: " ++ show x
parseExDate :: Content -> ContentParser ExDate
parseExDate (ContentLine _ "EXDATE" o bs) = do
(typ, tzid, o') <- typTzIdO o
let bs' = B.split ',' bs
case typ of
"DATE-TIME" -> do xs <- mapM (parseDateTime tzid) bs'
return . ExDateTimes (S.fromList xs) $ toO o'
"DATE" -> do xs <- mapM parseDate bs'
return . ExDates (S.fromList xs) $ toO o'
_ -> throwError $ "Invalid type: " ++ show typ
parseExDate x = throwError $ "parseExDate: " ++ show x
parseCategories :: Content -> ContentParser Categories
parseCategories (ContentLine _ "CATEGORIES" o bs) = do
vals <- parseText bs
lang <- mapM paramOnlyOne $ Language . CI.mk .: lookup "LANGUAGE" o
let o' = filter (\(x, _) -> x `notElem` ["LANGUAGE"]) o
return $ Categories (S.fromList vals) lang (toO o')
parseCategories x = throwError $ "parseCategories: " ++ show x
parseAttendee :: Content -> ContentParser Attendee
parseAttendee (ContentLine _ "ATTENDEE" o bs) = do
attendeeValue <- parseURI =<< asks (T.unpack . ($ bs) . dfBS2Text)
attendeeCUType <- g (parseCUType . CI.mk .: paramOnlyOne) $
lookup "CUTYPE" o
attendeeMember <- g (S.fromList .: mapM (parseURI . T.unpack)) $
lookup "MEMBER" o
attendeeRole <- g (parseRole . CI.mk .: paramOnlyOne) $ lookup "ROLE" o
attendeePartStat <- g (parsePartStat . CI.mk .: paramOnlyOne) $
lookup "PARTSTAT" o
attendeeRSVP <- maybe (return False) (parseBool . CI.mk <=< paramOnlyOne) $
lookup "RSVP" o
attendeeDelTo <- g (S.fromList .: mapM (parseURI . T.unpack)) $
lookup "DELEGATED-TO" o
attendeeDelFrom <- g (S.fromList .: mapM (parseURI . T.unpack)) $
lookup "DELEGATED-FROM" o
attendeeSentBy <- mapM (parseURI . T.unpack <=< paramOnlyOne) $
lookup "SENT-BY" o
attendeeCN <- mapM paramOnlyOne $ lookup "CN" o
attendeeDir <- mapM (parseURI . T.unpack <=< paramOnlyOne) $
lookup "DIR" o
attendeeLanguage <- mapM (Language . CI.mk .: paramOnlyOne) $
lookup "LANGUAGE" o
let attendeeOther = toO $ filter f o
f (x, _) = x `notElem` [ "CUTYPE", "MEMBER", "ROLE", "PARTSTAT", "RSVP"
, "DELEGATED-TO", "DELEGATED-FROM", "SENT-BY"
, "CN", "DIR"]
return Attendee {..}
where g :: (Monad m, Default b) => (a -> m b) -> Maybe a -> m b
g = maybe $ return def
parseAttendee x = throwError $ "parseAttendee: " ++ show x
parseAttachment :: Content -> ContentParser Attachment
parseAttachment (ContentLine _ "ATTACH" o bs) = do
fmt <- mapM (parseMime <=< paramOnlyOne) $ lookup "FMTTYPE" o
val <- mapM paramOnlyOne $ lookup "VALUE" o
case val of
Just "BINARY" -> do
enc <- mapM paramOnlyOne $ lookup "ENCODING" o
case enc of
Just "BASE64" ->
case B64.decode bs of
Left e -> throwError $ "parseAttachment: invalid \
\base64: " ++ e
Right v -> return $ BinaryAttachment fmt v
(toO $ filter binF o)
_ -> throwError $ "parseAttachment: invalid encoding: " ++
show enc
Nothing -> do uri <- parseURI =<< asks (T.unpack . ($ bs) . dfBS2Text)
return $ UriAttachment fmt uri (toO $ filter f o)
_ -> throwError $ "parseAttachment: invalid value: " ++ show val
where binF a@(x, _) = f a && x /= "VALUE" && x /= "ENCODING"
f (x, _) = x /= "FMTTYPE"
parseAttachment x = throwError $ "parseAttachment: " ++ show x
parseDurationProp :: Maybe DTStart -> Content -> ContentParser DurationProp
parseDurationProp dts (ContentLine _ "DURATION" o bs) = do
val <- parseDuration "DURATION" bs
case (dts, val) of
(Just DTStartDate {}, DurationDate {..})
| durHour == 0 && durMinute == 0 && durSecond == 0 -> return ()
(Just DTStartDate {}, DurationWeek {}) -> return ()
(Just DTStartDate {}, _) ->
throwError "DURATION must be in weeks or days when DTSTART \
\has VALUE DATE and not DATE-TIME."
_ -> return ()
return . DurationProp val $ toO o
parseDurationProp _ x = throwError $ "parseDurationProp: " ++ show x
parseRecurId :: Maybe DTStart -> Content -> ContentParser RecurrenceId
parseRecurId dts (ContentLine p "RECURRENCE-ID" o bs) = do
range' <- mapM (parseRange . CI.mk <=< paramOnlyOne) $ lookup "RANGE" o
recurid <- parseSimpleDateOrDateTime
(($ range') . RecurrenceIdDateTime)
(($ range') . RecurrenceIdDate)
(ContentLine p "RECURRENCE-ID" (filter ((/="RANGE").fst) o) bs)
case (dts, recurid) of
(Nothing, _) -> return recurid
(Just DTStartDate {}, RecurrenceIdDate {}) ->
return recurid
(Just DTStartDateTime {dtStartDateTimeValue = v},
RecurrenceIdDateTime {recurrenceIdDateTime = r}) ->
case (v, r) of
(UTCDateTime {}, FloatingDateTime {}) -> err dts recurid
(UTCDateTime {}, ZonedDateTime {}) -> err dts recurid
(FloatingDateTime {}, UTCDateTime {}) -> err dts recurid
(ZonedDateTime {}, UTCDateTime {}) -> err dts recurid
_ -> return recurid
_ -> err dts recurid
where err d r = throwError $ "parseRecurId: DTSTART local time mismatch: " ++
show (d, r)
parseRecurId _ x = throwError $ "parseRecurId: " ++ show x
parseTransp :: Content -> ContentParser TimeTransparency
parseTransp (ContentLine _ "TRANSP" o x)
| CI.mk x == "OPAQUE" = return . Opaque $ toO o
| CI.mk x == "TRANSPARENT" = return . Transparent $ toO o
parseTransp x = throwError $ "parseTransp: " ++ show x
parseEventStatus :: Content -> ContentParser EventStatus
parseEventStatus (ContentLine _ "STATUS" o x)
| CI.mk x == "TENTATIVE" = return . TentativeEvent $ toO o
| CI.mk x == "CONFIRMED" = return . ConfirmedEvent $ toO o
| CI.mk x == "CANCELLED" = return . CancelledEvent $ toO o
parseEventStatus x = throwError $ "parseEventStatus: " ++ show x
parseTodoStatus :: Content -> ContentParser TodoStatus
parseTodoStatus (ContentLine _ "STATUS" o x)
| CI.mk x == "NEEDS-ACTION" = return . TodoNeedsAction $ toO o
| CI.mk x == "COMPLETED" = return . CompletedTodo $ toO o
| CI.mk x == "IN-PROCESS" = return . InProcessTodo $ toO o
| CI.mk x == "CANCELLED" = return . CancelledTodo $ toO o
parseTodoStatus x = throwError $ "parseTodoStatus: " ++ show x
parseJournalStatus :: Content -> ContentParser JournalStatus
parseJournalStatus (ContentLine _ "STATUS" o x)
| CI.mk x == "DRAFT" = return . DraftJournal $ toO o
| CI.mk x == "FINAL" = return . FinalJournal $ toO o
| CI.mk x == "CANCELLED" = return . CancelledJournal $ toO o
parseJournalStatus x = throwError $ "parseJournalStatus: " ++ show x
parseOrganizer :: Content -> ContentParser Organizer
parseOrganizer (ContentLine _ "ORGANIZER" o bs) = do
organizerValue <- parseURI =<< asks (T.unpack . ($ bs) . dfBS2Text)
organizerCN <- mapM paramOnlyOne $ lookup "CN" o
organizerDir <- mapM (parseURI . T.unpack <=< paramOnlyOne) $ lookup "DIR" o
organizerSentBy <- mapM (parseURI . T.unpack <=< paramOnlyOne) $
lookup "SENT-BY" o
organizerLanguage <- mapM (Language . CI.mk .: paramOnlyOne) $
lookup "LANGUAGE" o
let f x = x `notElem` ["CN", "DIR", "SENT-BY", "LANGUAGE"]
o' = filter (f . fst) o
return Organizer { organizerOther = toO o', .. }
parseOrganizer x = throwError $ "parseOrganizer: " ++ show x
parseGeo :: Content -> ContentParser Geo
parseGeo (ContentLine _ "GEO" o bs) = do
let (lat', long') = B.break (==';') bs
lat = maybeRead . stripPlus $ B.unpack lat' :: Maybe Float
long = maybeRead . stripPlus . B.unpack $ B.tail long' :: Maybe Float
when (B.null long' || isNothing (lat >> long)) .
throwError $ "Invalid latitude/longitude: " ++ show bs
return $ Geo (fromJust lat) (fromJust long) (toO o)
where stripPlus ('+':xs) = xs
stripPlus xs = xs
parseGeo x = throwError $ "parseGeo: " ++ show x
parseClass :: Content -> ContentParser Class
parseClass (ContentLine _ "CLASS" o bs) = do
iconv <- asks dfBS2IText
return . flip Class (toO o) $
case iconv bs of
"PUBLIC" -> Public
"PRIVATE" -> Private
"CONFIDENTIAL" -> Confidential
x -> ClassValueX x
parseClass x = throwError $ "parseClass: " ++ show x
parseTZName :: Content -> ContentParser TZName
parseTZName (ContentLine _ "TZNAME" o bs) = do
txt <- valueOnlyOne =<< parseText bs
lang <- mapM paramOnlyOne $ Language . CI.mk .: lookup "LANGUAGE" o
return $ TZName txt lang (toO o)
parseTZName x = throwError $ "parseTZName: " ++ show x
parseVersion :: Content -> ContentParser ICalVersion
parseVersion (ContentLine _ "VERSION" o bs) = do
c <- asks dfBS2Text
let (maxver', minver'') = break (==';') . T.unpack $ c bs
minver' = drop 1 minver''
parseVer = fst .: listToMaybe . filter ((=="") . snd)
. readP_to_S Ver.parseVersion
maxver = parseVer maxver'
minver = parseVer minver'
[maxJ, minJ] = fromJust <$> [maxver, minver]
when (isNothing maxver) .
throwError $ "parseVersion: error parsing version: " ++ show maxver'
if null minver''
then return $ MaxICalVersion maxJ (toO o)
else do when (isNothing minver) .
throwError $ "parseVersion: error parsing version: "
++ show minver'
return $ MinMaxICalVersion maxJ minJ (toO o)
parseVersion x = throwError $ "parseVersion: " ++ show x
parseTZID :: Content -> ContentParser TZID
parseTZID (ContentLine _ "TZID" o bs) = do
tzidValue <- asks $ ($ bs) . dfBS2Text
let tzidGlobal = (fst <$> T.uncons tzidValue) == Just '/'
tzidOther = toO o
return TZID {..}
parseTZID x = throwError $ "parseTZID: " ++ show x
parseRRule :: Maybe DTStart -> Content -> ContentParser RRule
parseRRule Nothing _ = throwError "parseRRule: missing DTSTART."
parseRRule (Just dts) (ContentLine _ "RRULE" o bs) =
case runParser (parseRecur dts) def "RRULE" bs of
Left e -> throwError $ show e
Right x -> do y <- x
return . RRule y $ toO o
parseRRule _ x = throwError $ "parseRRule: " ++ show x
parseCreated :: Content -> ContentParser Created
parseCreated (ContentLine _ "CREATED" o bs) = do
createdValue <- mustBeUTC =<< parseDateTime Nothing bs
let createdOther = toO o
return Created {..}
parseCreated x = throwError $ "parseCreated: " ++ show x
parseLastModified :: Content -> ContentParser LastModified
parseLastModified (ContentLine _ "LAST-MODIFIED" o bs) = do
lastModifiedValue <- mustBeUTC =<< parseDateTime Nothing bs
let lastModifiedOther = toO o
return LastModified {..}
parseLastModified x = throwError $ "parseLastModified: " ++ show x
parseRDate :: Content -> ContentParser RDate
parseRDate c@(ContentLine _ "RDATE" o bs) = do
typ <- paramOnlyOne . fromMaybe ["DATE-TIME"] $ lookup "VALUE" o
case typ of
"PERIOD" -> do
tzid <- mapM paramOnlyOne $ lookup "TZID" o
p <- S.fromList .: mapM (parsePeriod tzid) $ B.split ',' bs
return . RDatePeriods p . toO $
filter ((`notElem` ["VALUE", "TZID"]) . fst) o
_ -> parseSimpleDatesOrDateTimes RDateDateTimes RDateDates c
parseRDate x = throwError $ "parseRDate: " ++ show x
parseUTCOffset :: Content -> ContentParser UTCOffset
parseUTCOffset (ContentLine _ n o bs)
| n `elem` ["TZOFFSETTO", "TZOFFSETFROM"] = do
let str = B.unpack bs
(s:rest) = str
(t1:t2:m1:m2:sec) = map digitToInt rest
(s1:s2:_) = sec
sign x = if s == '-' then negate x else x
when (length str < 5 || any (not . isDigit) rest || s `notElem` ['+','-']
|| length sec `notElem` [0,2]) .
throwError $ "parseUTCOffset: " ++ str
return . UTCOffset (sign $ ((t1 * 10 + t2) * 60 + (m1 * 10 + m2)) * 60 +
if not (null sec) then s1 * 10 + s2 else 0)
$ toO o
parseUTCOffset x = throwError $ "parseUTCOffset: " ++ show x