formvalidation.go (10054B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package validate 19 20 import ( 21 "errors" 22 "fmt" 23 "net/mail" 24 "strings" 25 26 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 27 "github.com/superseriousbusiness/gotosocial/internal/config" 28 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 29 "github.com/superseriousbusiness/gotosocial/internal/regexes" 30 pwv "github.com/wagslane/go-password-validator" 31 "golang.org/x/text/language" 32 ) 33 34 const ( 35 maximumPasswordLength = 256 36 minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator 37 minimumReasonLength = 40 38 maximumReasonLength = 500 39 maximumSiteTitleLength = 40 40 maximumShortDescriptionLength = 500 41 maximumDescriptionLength = 5000 42 maximumSiteTermsLength = 5000 43 maximumUsernameLength = 64 44 maximumEmojiCategoryLength = 64 45 maximumProfileFieldLength = 255 46 maximumProfileFields = 6 47 maximumListTitleLength = 200 48 ) 49 50 // NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. 51 func NewPassword(password string) error { 52 if password == "" { 53 return errors.New("no password provided") 54 } 55 56 if len([]rune(password)) > maximumPasswordLength { 57 return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength) 58 } 59 60 if err := pwv.Validate(password, minimumPasswordEntropy); err != nil { 61 // Modify error message to include percentage requred entropy the password has 62 percent := int(100 * pwv.GetEntropy(password) / minimumPasswordEntropy) 63 return errors.New(strings.ReplaceAll( 64 err.Error(), 65 "insecure password", 66 fmt.Sprintf("password is only %d%% strength", percent))) 67 } 68 69 return nil // pasword OK 70 } 71 72 // Username makes sure that a given username is valid (ie., letters, numbers, underscores, check length). 73 // Returns an error if not. 74 func Username(username string) error { 75 if username == "" { 76 return errors.New("no username provided") 77 } 78 79 if !regexes.Username.MatchString(username) { 80 return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max %d characters", username, maximumUsernameLength) 81 } 82 83 return nil 84 } 85 86 // Email makes sure that a given email address is a valid address. 87 // Returns an error if not. 88 func Email(email string) error { 89 if email == "" { 90 return errors.New("no email provided") 91 } 92 93 _, err := mail.ParseAddress(email) 94 return err 95 } 96 97 // Language checks that the given language string is a 2- or 3-letter ISO 639 code. 98 // Returns an error if the language cannot be parsed. See: https://pkg.go.dev/golang.org/x/text/language 99 func Language(lang string) error { 100 if lang == "" { 101 return errors.New("no language provided") 102 } 103 _, err := language.ParseBase(lang) 104 return err 105 } 106 107 // SignUpReason checks that a sufficient reason is given for a server signup request 108 func SignUpReason(reason string, reasonRequired bool) error { 109 if !reasonRequired { 110 // we don't care! 111 // we're not going to do anything with this text anyway if no reason is required 112 return nil 113 } 114 115 if reason == "" { 116 return errors.New("no reason provided") 117 } 118 119 length := len([]rune(reason)) 120 121 if length < minimumReasonLength { 122 return fmt.Errorf("reason should be at least %d chars but '%s' was %d", minimumReasonLength, reason, length) 123 } 124 125 if length > maximumReasonLength { 126 return fmt.Errorf("reason should be no more than %d chars but given reason was %d", maximumReasonLength, length) 127 } 128 return nil 129 } 130 131 // DisplayName checks that a requested display name is valid 132 func DisplayName(displayName string) error { 133 // TODO: add some validation logic here -- length, characters, etc 134 return nil 135 } 136 137 // Note checks that a given profile/account note/bio is valid 138 func Note(note string) error { 139 // TODO: add some validation logic here -- length, characters, etc 140 return nil 141 } 142 143 // Privacy checks that the desired privacy setting is valid 144 func Privacy(privacy string) error { 145 if privacy == "" { 146 return fmt.Errorf("empty string for privacy not allowed") 147 } 148 switch apimodel.Visibility(privacy) { 149 case apimodel.VisibilityDirect, apimodel.VisibilityMutualsOnly, apimodel.VisibilityPrivate, apimodel.VisibilityPublic, apimodel.VisibilityUnlisted: 150 return nil 151 } 152 return fmt.Errorf("privacy '%s' was not recognized, valid options are 'direct', 'mutuals_only', 'private', 'public', 'unlisted'", privacy) 153 } 154 155 // StatusContentType checks that the desired status format setting is valid. 156 func StatusContentType(statusContentType string) error { 157 if statusContentType == "" { 158 return fmt.Errorf("empty string for status format not allowed") 159 } 160 switch apimodel.StatusContentType(statusContentType) { 161 case apimodel.StatusContentTypePlain, apimodel.StatusContentTypeMarkdown: 162 return nil 163 } 164 return fmt.Errorf("status content type '%s' was not recognized, valid options are 'text/plain', 'text/markdown'", statusContentType) 165 } 166 167 func CustomCSS(customCSS string) error { 168 if !config.GetAccountsAllowCustomCSS() { 169 return errors.New("accounts-allow-custom-css is not enabled for this instance") 170 } 171 172 maximumCustomCSSLength := config.GetAccountsCustomCSSLength() 173 if length := len([]rune(customCSS)); length > maximumCustomCSSLength { 174 return fmt.Errorf("custom_css must be less than %d characters, but submitted custom_css was %d characters", maximumCustomCSSLength, length) 175 } 176 177 return nil 178 } 179 180 // EmojiShortcode just runs the given shortcode through the regular expression 181 // for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters, 182 // a-zA-Z, numbers, and underscores. 183 func EmojiShortcode(shortcode string) error { 184 if !regexes.EmojiShortcode.MatchString(shortcode) { 185 return fmt.Errorf("shortcode %s did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only", shortcode) 186 } 187 return nil 188 } 189 190 // EmojiCategory validates the length of the given category string. 191 func EmojiCategory(category string) error { 192 if length := len(category); length > maximumEmojiCategoryLength { 193 return fmt.Errorf("emoji category %s did not pass validation, must be less than %d characters, but provided value was %d characters", category, maximumEmojiCategoryLength, length) 194 } 195 return nil 196 } 197 198 // SiteTitle ensures that the given site title is within spec. 199 func SiteTitle(siteTitle string) error { 200 if length := len([]rune(siteTitle)); length > maximumSiteTitleLength { 201 return fmt.Errorf("site title should be no more than %d chars but given title was %d", maximumSiteTitleLength, length) 202 } 203 204 return nil 205 } 206 207 // SiteShortDescription ensures that the given site short description is within spec. 208 func SiteShortDescription(d string) error { 209 if length := len([]rune(d)); length > maximumShortDescriptionLength { 210 return fmt.Errorf("short description should be no more than %d chars but given description was %d", maximumShortDescriptionLength, length) 211 } 212 213 return nil 214 } 215 216 // SiteDescription ensures that the given site description is within spec. 217 func SiteDescription(d string) error { 218 if length := len([]rune(d)); length > maximumDescriptionLength { 219 return fmt.Errorf("description should be no more than %d chars but given description was %d", maximumDescriptionLength, length) 220 } 221 222 return nil 223 } 224 225 // SiteTerms ensures that the given site terms string is within spec. 226 func SiteTerms(t string) error { 227 if length := len([]rune(t)); length > maximumSiteTermsLength { 228 return fmt.Errorf("terms should be no more than %d chars but given terms was %d", maximumSiteTermsLength, length) 229 } 230 231 return nil 232 } 233 234 // ULID returns true if the passed string is a valid ULID. 235 func ULID(i string) bool { 236 return regexes.ULID.MatchString(i) 237 } 238 239 // ProfileFields validates the length of provided fields slice, 240 // and also iterates through the fields and trims each name + value 241 // to maximumProfileFieldLength, if they were above. 242 func ProfileFields(fields []*gtsmodel.Field) error { 243 if len(fields) > maximumProfileFields { 244 return fmt.Errorf("cannot have more than %d profile fields", maximumProfileFields) 245 } 246 247 // Trim each field name + value to maximum allowed length. 248 for _, field := range fields { 249 n := []rune(field.Name) 250 if len(n) > maximumProfileFieldLength { 251 field.Name = string(n[:maximumProfileFieldLength]) 252 } 253 254 v := []rune(field.Value) 255 if len(v) > maximumProfileFieldLength { 256 field.Value = string(v[:maximumProfileFieldLength]) 257 } 258 } 259 260 return nil 261 } 262 263 // ListTitle validates the title of a new or updated List. 264 func ListTitle(title string) error { 265 if title == "" { 266 return fmt.Errorf("list title must be provided, and must be no more than %d chars", maximumListTitleLength) 267 } 268 269 if length := len([]rune(title)); length > maximumListTitleLength { 270 return fmt.Errorf("list title length must be no more than %d chars, provided title was %d chars", maximumListTitleLength, length) 271 } 272 273 return nil 274 } 275 276 // ListRepliesPolicy validates the replies_policy of a new or updated list. 277 func ListRepliesPolicy(repliesPolicy gtsmodel.RepliesPolicy) error { 278 switch repliesPolicy { 279 case "", gtsmodel.RepliesPolicyFollowed, gtsmodel.RepliesPolicyList, gtsmodel.RepliesPolicyNone: 280 // No problem. 281 return nil 282 default: 283 // Uh oh. 284 return fmt.Errorf("list replies_policy must be either empty or one of 'followed', 'list', 'none'") 285 } 286 }