Skip to content

Commit b21f35b

Browse files
authored
Fix #1076: separate validators from UI and doctest them (#1077)
1 parent 2d4f3f7 commit b21f35b

File tree

3 files changed

+168
-29
lines changed

3 files changed

+168
-29
lines changed

hackage-server.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ library lib-server
247247
Distribution.Server.Util.Parse
248248
Distribution.Server.Util.ServeTarball
249249
Distribution.Server.Util.Validators
250+
Distribution.Server.Util.Validators.Internal
250251
-- [unused] Distribution.Server.Util.TarIndex
251252
Distribution.Server.Util.GZip
252253
Distribution.Server.Util.ContentType

src/Distribution/Server/Util/Validators.hs

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,26 @@ module Distribution.Server.Util.Validators
44
, guardValidLookingEmail
55
) where
66

7-
import Data.Char (isSpace, isPrint)
8-
import qualified Data.Text as T
7+
import Data.Text (Text)
8+
import Distribution.Pretty (prettyShow)
99

1010
import Distribution.Server.Framework
11-
import Distribution.Server.Users.Types (isValidUserNameChar)
11+
import Distribution.Server.Util.Validators.Internal (validName, validUserName, validEmail)
1212

13-
guardValidLookingName :: T.Text -> ServerPartE ()
14-
guardValidLookingName str = either errBadUserName return $ do
15-
guard (T.length str <= 70) ?! "Sorry, we didn't expect names to be longer than 70 characters."
16-
guard (T.all isPrint str) ?! "Unexpected character in name, please use only printable Unicode characters."
13+
guardValidLookingName :: Text -> ServerPartE ()
14+
guardValidLookingName =
15+
either (errBadUserName . prettyShow) return . validName
1716

18-
guardValidLookingUserName :: T.Text -> ServerPartE ()
19-
guardValidLookingUserName str = either errBadRealName return $ do
20-
guard (T.length str <= 50) ?! "Sorry, we didn't expect login names to be longer than 50 characters."
21-
guard (T.all isValidUserNameChar str) ?! "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols."
17+
guardValidLookingUserName :: Text -> ServerPartE ()
18+
guardValidLookingUserName =
19+
either (errBadRealName . prettyShow) return . validUserName
2220

2321
-- Make sure this roughly corresponds to the frontend validation in user-details-form.html.st
24-
guardValidLookingEmail :: T.Text -> ServerPartE ()
25-
guardValidLookingEmail str = either errBadEmail return $ do
26-
guard (T.length str <= 100) ?! "Sorry, we didn't expect email addresses to be longer than 100 characters."
27-
guard (T.all isPrint str) ?! "Unexpected character in email address, please use only printable Unicode characters."
28-
guard hasAtSomewhere ?! "Oops, that doesn't look like an email address."
29-
guard (T.all (not.isSpace) str) ?! "Oops, no spaces in email addresses please."
30-
guard (T.all (not.isAngle) str) ?! "Please use just the email address, not \"name\" <[email protected]> style."
31-
where
32-
isAngle c = c == '<' || c == '>'
33-
hasAtSomewhere = case T.span (/= '@') str of
34-
(before, rest)
35-
| Just (_, after) <- T.uncons rest ->
36-
T.length before >= 1
37-
&& T.length after > 0
38-
&& not ('@' `T.elem` after)
39-
_ -> False
22+
guardValidLookingEmail :: Text -> ServerPartE ()
23+
guardValidLookingEmail =
24+
either (errBadEmail . prettyShow) return . validEmail
4025

4126
errBadUserName, errBadRealName, errBadEmail :: String -> ServerPartE a
42-
errBadUserName err = errBadRequest "Problem with login name" [MText err]
43-
errBadRealName err = errBadRequest "Problem with name" [MText err]
27+
errBadUserName err = errBadRequest "Problem with login name" [MText err]
28+
errBadRealName err = errBadRequest "Problem with name" [MText err]
4429
errBadEmail err = errBadRequest "Problem with email address" [MText err]
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
-- | Purely functional version of "Distribution.Server.Util.Validators"
5+
-- for testing the validators.
6+
7+
module Distribution.Server.Util.Validators.Internal where
8+
9+
import Control.Monad (unless)
10+
import Control.Monad.Except (MonadError(..))
11+
12+
import Data.Char (isSpace, isPrint)
13+
import Data.Text (Text)
14+
import qualified Data.Text as T
15+
16+
import Distribution.Pretty (Pretty(..))
17+
import Distribution.Server.Users.Types (isValidUserNameChar)
18+
19+
-- Set up doctest to deal with text literals.
20+
21+
-- $setup
22+
-- >>> :set -XOverloadedStrings
23+
24+
-- | Basic sanity checking on names.
25+
--
26+
-- >>> validName "Innocent User"
27+
-- Right ()
28+
--
29+
-- >>> validName "Mr. X is the greatest super duper dude of all!"
30+
-- Right ()
31+
--
32+
-- >>> validName "I am also a developer, maintainer, blogger, for Haskell, Hackage, Cabal, Stackage"
33+
-- Left NameTooLong
34+
--
35+
-- >>> validName "My name has beeps \BEL, newlines \n, and \t tabs"
36+
-- Left NameNotPrintable
37+
--
38+
validName :: Text -> Either InvalidName ()
39+
validName str = do
40+
unless (T.length str <= 70) $ throwError NameTooLong
41+
unless (T.all isPrint str) $ throwError NameNotPrintable
42+
43+
-- | Errors produced by 'validName' check.
44+
45+
data InvalidName
46+
= NameTooLong -- ^ More than 70 characters long.
47+
| NameNotPrintable -- ^ Contains unprintable characters.
48+
deriving (Eq, Show)
49+
50+
instance Pretty InvalidName where
51+
pretty = \case
52+
NameTooLong -> "Sorry, we didn't expect names to be longer than 70 characters."
53+
NameNotPrintable -> "Unexpected character in name, please use only printable Unicode characters."
54+
55+
-- | Basic sanity checking on user names.
56+
--
57+
-- >>> validUserName "innocent_user_42"
58+
-- Right ()
59+
--
60+
-- >>> validUserName "mr_X_stretches_the_Limit_of_50_characters_01234567"
61+
-- Right ()
62+
--
63+
-- >>> validUserName "01234"
64+
-- Right ()
65+
--
66+
-- >>> validUserName "dashes-not-allowed"
67+
-- Left UserNameInvalidChar
68+
--
69+
-- >>> validUserName "questions_not_allowed?"
70+
-- Left UserNameInvalidChar
71+
--
72+
-- >>> validUserName "my_Ego_busts_the_Limit_of_50_characters_01234567890"
73+
-- Left UserNameTooLong
74+
--
75+
validUserName :: T.Text -> Either InvalidUserName ()
76+
validUserName str = do
77+
unless (T.length str <= 50) $ throwError UserNameTooLong
78+
unless (T.all isValidUserNameChar str) $ throwError UserNameInvalidChar
79+
80+
-- | Errors produced by 'validUserName' check.
81+
82+
data InvalidUserName
83+
= UserNameTooLong -- ^ More than 50 characters long.
84+
| UserNameInvalidChar -- ^ Contains character not matching 'isValidUserNameChar'.
85+
deriving (Eq, Show)
86+
87+
instance Pretty InvalidUserName where
88+
pretty = \case
89+
UserNameTooLong -> "Sorry, we didn't expect login names to be longer than 50 characters."
90+
UserNameInvalidChar -> "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols."
91+
92+
-- | Basic sanity checking in email.
93+
--
94+
-- >>> validEmail "[email protected]"
95+
-- Right ()
96+
--
97+
-- >>> validEmail "[email protected]"
98+
-- Right ()
99+
--
100+
-- >>> validEmail "Emmanuel.Lauterbachs.Cousin@mailrelay.tor.amazon-aws.bill-me.cold-fusion.bogus-domain.phantasy-promi.darknet.de"
101+
-- Left EmailTooLong
102+
--
103+
-- >>> validEmail "\BELlingcat@a\nonymous.\to"
104+
-- Left EmailNotPrintable
105+
--
106+
-- >>> validEmail "ich-im-aether"
107+
-- Left EmailBadFormat
108+
--
109+
-- >>> validEmail "ich@guuugle@kom"
110+
-- Left EmailBadFormat
111+
--
112+
-- >>> validEmail "Windows User @ Company . com"
113+
-- Left EmailHasSpace
114+
--
115+
-- >>> validEmail "Name<[email protected]>"
116+
-- Left EmailHasAngle
117+
--
118+
validEmail :: T.Text -> Either InvalidEmail ()
119+
validEmail str = do
120+
unless (T.length str <= 100) $ throwError EmailTooLong
121+
unless (T.all isPrint str) $ throwError EmailNotPrintable
122+
unless hasAtSomewhere $ throwError EmailBadFormat
123+
unless (T.all (not.isSpace) str) $ throwError EmailHasSpace
124+
unless (T.all (not.isAngle) str) $ throwError EmailHasAngle
125+
where
126+
isAngle c = c == '<' || c == '>'
127+
hasAtSomewhere = case T.break (== '@') str of
128+
(before, rest)
129+
| Just (_, after) <- T.uncons rest ->
130+
not $ or
131+
[ T.null before
132+
, T.null after
133+
, '@' `T.elem` after
134+
]
135+
| otherwise -> False
136+
137+
-- | Errors produced by 'validEmail' check.
138+
139+
data InvalidEmail
140+
= EmailTooLong -- ^ More than 100 characters long.
141+
| EmailNotPrintable -- ^ Contains unprintable characters.
142+
| EmailBadFormat -- ^ Doesn't have exactly one @ sign.
143+
| EmailHasSpace -- ^ Contains spaces.
144+
| EmailHasAngle -- ^ Contains angle brackets.
145+
deriving (Eq, Show)
146+
147+
instance Pretty InvalidEmail where
148+
pretty = \case
149+
EmailTooLong -> "Sorry, we didn't expect email addresses to be longer than 100 characters."
150+
EmailNotPrintable -> "Unexpected character in email address, please use only printable Unicode characters."
151+
EmailBadFormat -> "Oops, that doesn't look like an email address."
152+
EmailHasSpace -> "Oops, no spaces in email addresses please."
153+
EmailHasAngle -> "Please use just the email address, not \"name\" <[email protected]> style."

0 commit comments

Comments
 (0)