-- SPDX-FileCopyrightText: 2022 Gregor Kleen ,Steffen Jost -- -- SPDX-License-Identifier: AGPL-3.0-or-later -- TODO: why is this Handler.Utils.Profile instead of Utils.Profile? -- TODO: consider merging with Handler.Utils.Users? module Handler.Utils.Profile ( validDisplayName, checkDisplayName, fixDisplayName , validPostAddress , validEmail, validEmail', pickValidEmail, pickValidEmail' , validFraportPersonalNumber ) where import Import.NoFoundation import Data.Char import qualified Data.Text as Text import qualified Data.Text.Lazy as LT import qualified Data.CaseInsensitive as CI import qualified Data.MultiSet as MultiSet import qualified Data.Set as Set import qualified Text.Email.Validate as Email -- | Instead of CI.mk, this still allows use of Text.isInfixOf, etc. stripFold :: Text -> Text stripFold = Text.toCaseFold . Text.strip -- | remove last comma and swap order of the two parts, ie. transforming "surname, givennames" into "givennames surname". -- Input "givennames surname" is left unchanged, except for removing excess whitespace fixDisplayName :: UserDisplayName -> UserDisplayName fixDisplayName udn = let (Text.strip . Text.dropEnd 1 -> surname, Text.strip -> firstnames) = Text.breakOnEnd "," udn in Text.toTitle $ Text.strip $ firstnames <> Text.cons ' ' surname -- | Like `validDisplayName` but may return an automatically corrected name checkDisplayName :: Maybe UserTitle -> UserFirstName -> UserSurname -> UserDisplayName -> Maybe UserDisplayName checkDisplayName mTitle fName sName (fixDisplayName -> dName) | validDisplayName mTitle fName sName dName = Just dName | otherwise = Nothing validDisplayName :: Maybe UserTitle -> UserFirstName -> UserSurname -> UserDisplayName -> Bool validDisplayName (fmap stripFold -> mTitle) (stripFold -> fName) (stripFold -> sName) (stripFold -> dName) = and [ dNameFrags `MultiSet.isSubsetOf` MultiSet.unions [titleFrags, fNameFrags, sNameFrags] , sName `Text.isInfixOf` dName , all ((<= 1) . Text.length) . filter (Text.any isAdd) $ Text.group dName , dNameLetters `Set.isSubsetOf` Set.unions [titleLetters, fNameLetters, sNameLetters, addLetters] ] where titleFrags = foldMap makeMultiSet mTitle dNameFrags = makeMultiSet dName fNameFrags = makeMultiSet fName sNameFrags = makeMultiSet sName titleLetters = foldMap (Set.fromList . unpack) mTitle fNameLetters = Set.fromList $ unpack fName sNameLetters = Set.fromList $ unpack sName dNameLetters = Set.fromList $ unpack dName addLetters = Set.fromList [' ', '.', '-'] isAdd = (`Set.member` addLetters) splitAdd = Text.split isAdd makeMultiSet = MultiSet.fromList . filter (not . Text.null) . splitAdd -- | Primitive postal address requires at least one alphabetic character, one digit and a line break validPostAddress :: Maybe StoredMarkup -> Bool validPostAddress (Just StoredMarkup {markupInput = addr}) | Just _ <- LT.find isLetter addr , Just _ <- LT.find isNumber addr -- , Just _ <- LT.find ((LineSeparator ==) . generalCategory) addr -- THIS DID NOT WORK , 1 < length (LT.lines addr) = True validPostAddress _ = False -- also see `Handler.Utils.Users.getEmailAddress` for Tests accepting User Type validEmail :: Email -> Bool -- Email = Text validEmail email = validRFC5322 && not invalidFraport where validRFC5322 = Email.isValid $ encodeUtf8 email invalidFraport = case Text.stripSuffix "@fraport.de" (foldCase email) of Just fralogin -> all isDigit $ drop 1 fralogin Nothing -> False validEmail' :: UserEmail -> Bool -- UserEmail = CI Text validEmail' = validEmail . CI.original -- | returns first argument, if it is a valid email address; returns second argument untested otherwise; convenience function pickValidEmail :: UserEmail -> UserEmail -> UserEmail pickValidEmail x y | validEmail' x = x | otherwise = y -- | returns first valid email address or none if none are valid pickValidEmail' :: UserEmail -> UserEmail -> Maybe UserEmail pickValidEmail' x y | validEmail' x = Just x | validEmail' y = Just y | otherwise = Nothing validFraportPersonalNumber :: Maybe Text -> Bool validFraportPersonalNumber Nothing = False validFraportPersonalNumber (Just t) | (Just pn) <- readMay t = pn >= (10000::Int) && pn <= (99999::Int) -- used to filter for SAP export | otherwise = False