2023-03-12 15:00:57 +00:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-04-01 18:46:45 +00:00
2021-09-01 16:29:25 +00:00
package validate
2021-04-01 18:46:45 +00:00
import (
"errors"
"fmt"
"net/mail"
2021-09-11 11:19:06 +00:00
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
2022-09-12 11:14:29 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2023-05-09 10:16:10 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-09-01 16:29:25 +00:00
"github.com/superseriousbusiness/gotosocial/internal/regexes"
2021-04-01 18:46:45 +00:00
pwv "github.com/wagslane/go-password-validator"
"golang.org/x/text/language"
)
2021-06-23 14:35:57 +00:00
const (
2023-07-21 09:29:18 +00:00
maximumPasswordLength = 72 // 72 bytes is the maximum length afforded by bcrypt. See https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword.
minimumPasswordEntropy = 60 // Heuristic for password strength. See https://github.com/wagslane/go-password-validator.
2021-06-23 14:35:57 +00:00
minimumReasonLength = 40
maximumReasonLength = 500
maximumSiteTitleLength = 40
maximumShortDescriptionLength = 500
maximumDescriptionLength = 5000
maximumSiteTermsLength = 5000
2021-09-01 16:29:25 +00:00
maximumUsernameLength = 64
2022-11-14 22:47:27 +00:00
maximumEmojiCategoryLength = 64
2023-03-06 09:30:19 +00:00
maximumProfileFieldLength = 255
2023-05-09 10:16:10 +00:00
maximumProfileFields = 6
2023-05-25 08:37:38 +00:00
maximumListTitleLength = 200
2024-03-06 10:15:58 +00:00
maximumFilterKeywordLength = 40
2021-06-23 14:35:57 +00:00
)
2023-07-23 10:33:17 +00:00
// Password returns a helpful error if the given password
2023-07-21 09:29:18 +00:00
// is too short, too long, or not sufficiently strong.
2023-07-23 10:33:17 +00:00
func Password ( password string ) error {
2023-07-21 09:29:18 +00:00
// Ensure length is OK first.
if pwLen := len ( password ) ; pwLen == 0 {
return errors . New ( "no password provided / provided password was 0 bytes" )
} else if pwLen > maximumPasswordLength {
return fmt . Errorf (
"password should be no more than %d bytes, provided password was %d bytes" ,
maximumPasswordLength , pwLen ,
)
2021-04-01 18:46:45 +00:00
}
2022-05-09 08:31:46 +00:00
if err := pwv . Validate ( password , minimumPasswordEntropy ) ; err != nil {
2023-07-21 09:29:18 +00:00
// Calculate the percentage of our desired entropy this password fulfils.
entropyPercent := int ( 100 * pwv . GetEntropy ( password ) / minimumPasswordEntropy )
// Replace the first 17 bytes (`insecure password`)
// of the error string with our own entropy message.
entropyMsg := fmt . Sprintf ( "password is only %d%% strength" , entropyPercent )
errMsg := entropyMsg + err . Error ( ) [ 17 : ]
return errors . New ( errMsg )
2022-05-09 08:31:46 +00:00
}
2023-07-09 16:25:37 +00:00
return nil // password OK
2021-04-01 18:46:45 +00:00
}
2021-09-01 16:29:25 +00:00
// Username makes sure that a given username is valid (ie., letters, numbers, underscores, check length).
2021-04-01 18:46:45 +00:00
// Returns an error if not.
2021-09-01 16:29:25 +00:00
func Username ( username string ) error {
2021-04-01 18:46:45 +00:00
if username == "" {
return errors . New ( "no username provided" )
}
2021-09-01 16:29:25 +00:00
if ! regexes . Username . MatchString ( username ) {
2021-06-23 14:35:57 +00:00
return fmt . Errorf ( "given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max %d characters" , username , maximumUsernameLength )
2021-04-01 18:46:45 +00:00
}
return nil
}
2021-09-01 16:29:25 +00:00
// Email makes sure that a given email address is a valid address.
2021-04-01 18:46:45 +00:00
// Returns an error if not.
2021-09-01 16:29:25 +00:00
func Email ( email string ) error {
2021-04-01 18:46:45 +00:00
if email == "" {
return errors . New ( "no email provided" )
}
_ , err := mail . ParseAddress ( email )
return err
}
2023-08-07 08:25:54 +00:00
// Language checks that the given language string is a valid, if not necessarily canonical, BCP 47 language tag.
// Returns a canonicalized version of the tag if the language can be parsed.
// Returns an error if the language cannot be parsed.
// See: https://pkg.go.dev/golang.org/x/text/language
func Language ( lang string ) ( string , error ) {
2021-04-01 18:46:45 +00:00
if lang == "" {
2023-08-07 08:25:54 +00:00
return "" , errors . New ( "no language provided" )
2021-04-01 18:46:45 +00:00
}
2023-08-07 08:25:54 +00:00
parsed , err := language . Parse ( lang )
if err != nil {
return "" , err
}
return parsed . String ( ) , err
2021-04-01 18:46:45 +00:00
}
2021-09-01 16:29:25 +00:00
// SignUpReason checks that a sufficient reason is given for a server signup request
func SignUpReason ( reason string , reasonRequired bool ) error {
2021-04-01 18:46:45 +00:00
if ! reasonRequired {
// we don't care!
// we're not going to do anything with this text anyway if no reason is required
return nil
}
if reason == "" {
return errors . New ( "no reason provided" )
}
2022-11-03 13:38:06 +00:00
length := len ( [ ] rune ( reason ) )
if length < minimumReasonLength {
return fmt . Errorf ( "reason should be at least %d chars but '%s' was %d" , minimumReasonLength , reason , length )
2021-04-01 18:46:45 +00:00
}
2022-11-03 13:38:06 +00:00
if length > maximumReasonLength {
return fmt . Errorf ( "reason should be no more than %d chars but given reason was %d" , maximumReasonLength , length )
2021-04-01 18:46:45 +00:00
}
return nil
}
2021-09-01 16:29:25 +00:00
// DisplayName checks that a requested display name is valid
func DisplayName ( displayName string ) error {
2021-04-01 18:46:45 +00:00
// TODO: add some validation logic here -- length, characters, etc
return nil
}
2021-09-01 16:29:25 +00:00
// Note checks that a given profile/account note/bio is valid
func Note ( note string ) error {
2021-04-01 18:46:45 +00:00
// TODO: add some validation logic here -- length, characters, etc
return nil
}
2021-09-01 16:29:25 +00:00
// Privacy checks that the desired privacy setting is valid
func Privacy ( privacy string ) error {
2021-09-11 11:19:06 +00:00
if privacy == "" {
return fmt . Errorf ( "empty string for privacy not allowed" )
}
switch apimodel . Visibility ( privacy ) {
case apimodel . VisibilityDirect , apimodel . VisibilityMutualsOnly , apimodel . VisibilityPrivate , apimodel . VisibilityPublic , apimodel . VisibilityUnlisted :
return nil
}
2022-08-06 10:09:21 +00:00
return fmt . Errorf ( "privacy '%s' was not recognized, valid options are 'direct', 'mutuals_only', 'private', 'public', 'unlisted'" , privacy )
}
2023-03-02 11:06:40 +00:00
// StatusContentType checks that the desired status format setting is valid.
func StatusContentType ( statusContentType string ) error {
if statusContentType == "" {
2022-08-06 10:09:21 +00:00
return fmt . Errorf ( "empty string for status format not allowed" )
}
2023-03-02 11:06:40 +00:00
switch apimodel . StatusContentType ( statusContentType ) {
case apimodel . StatusContentTypePlain , apimodel . StatusContentTypeMarkdown :
2022-08-06 10:09:21 +00:00
return nil
}
2023-03-02 11:06:40 +00:00
return fmt . Errorf ( "status content type '%s' was not recognized, valid options are 'text/plain', 'text/markdown'" , statusContentType )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
2022-09-12 11:14:29 +00:00
func CustomCSS ( customCSS string ) error {
if ! config . GetAccountsAllowCustomCSS ( ) {
return errors . New ( "accounts-allow-custom-css is not enabled for this instance" )
}
2023-05-25 13:18:15 +00:00
maximumCustomCSSLength := config . GetAccountsCustomCSSLength ( )
2022-11-03 13:38:06 +00:00
if length := len ( [ ] rune ( customCSS ) ) ; length > maximumCustomCSSLength {
2022-09-12 11:14:29 +00:00
return fmt . Errorf ( "custom_css must be less than %d characters, but submitted custom_css was %d characters" , maximumCustomCSSLength , length )
}
2023-05-25 13:18:15 +00:00
2022-09-12 11:14:29 +00:00
return nil
}
2021-09-01 16:29:25 +00:00
// EmojiShortcode just runs the given shortcode through the regular expression
2021-04-19 17:42:19 +00:00
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,
2023-06-02 15:42:14 +00:00
// a-zA-Z, numbers, and underscores.
2021-09-01 16:29:25 +00:00
func EmojiShortcode ( shortcode string ) error {
2024-02-20 10:46:04 +00:00
if ! regexes . EmojiValidator . MatchString ( shortcode ) {
2023-06-02 15:42:14 +00:00
return fmt . Errorf ( "shortcode %s did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only" , shortcode )
2021-04-19 17:42:19 +00:00
}
return nil
}
2021-06-23 14:35:57 +00:00
2022-11-14 22:47:27 +00:00
// EmojiCategory validates the length of the given category string.
func EmojiCategory ( category string ) error {
if length := len ( category ) ; length > maximumEmojiCategoryLength {
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 )
}
return nil
}
2021-09-01 16:29:25 +00:00
// SiteTitle ensures that the given site title is within spec.
func SiteTitle ( siteTitle string ) error {
2022-11-03 13:38:06 +00:00
if length := len ( [ ] rune ( siteTitle ) ) ; length > maximumSiteTitleLength {
return fmt . Errorf ( "site title should be no more than %d chars but given title was %d" , maximumSiteTitleLength , length )
2021-06-23 14:35:57 +00:00
}
return nil
}
2021-09-01 16:29:25 +00:00
// SiteShortDescription ensures that the given site short description is within spec.
func SiteShortDescription ( d string ) error {
2022-11-03 13:38:06 +00:00
if length := len ( [ ] rune ( d ) ) ; length > maximumShortDescriptionLength {
return fmt . Errorf ( "short description should be no more than %d chars but given description was %d" , maximumShortDescriptionLength , length )
2021-06-23 14:35:57 +00:00
}
return nil
}
2021-09-01 16:29:25 +00:00
// SiteDescription ensures that the given site description is within spec.
func SiteDescription ( d string ) error {
2022-11-03 13:38:06 +00:00
if length := len ( [ ] rune ( d ) ) ; length > maximumDescriptionLength {
return fmt . Errorf ( "description should be no more than %d chars but given description was %d" , maximumDescriptionLength , length )
2021-06-23 14:35:57 +00:00
}
return nil
}
2021-09-01 16:29:25 +00:00
// SiteTerms ensures that the given site terms string is within spec.
func SiteTerms ( t string ) error {
2022-11-03 13:38:06 +00:00
if length := len ( [ ] rune ( t ) ) ; length > maximumSiteTermsLength {
return fmt . Errorf ( "terms should be no more than %d chars but given terms was %d" , maximumSiteTermsLength , length )
2021-06-23 14:35:57 +00:00
}
return nil
}
2021-08-29 14:52:23 +00:00
2021-09-01 16:29:25 +00:00
// ULID returns true if the passed string is a valid ULID.
func ULID ( i string ) bool {
return regexes . ULID . MatchString ( i )
2021-08-29 14:52:23 +00:00
}
2023-03-06 09:30:19 +00:00
2023-05-09 10:16:10 +00:00
// ProfileFields validates the length of provided fields slice,
// and also iterates through the fields and trims each name + value
// to maximumProfileFieldLength, if they were above.
func ProfileFields ( fields [ ] * gtsmodel . Field ) error {
if len ( fields ) > maximumProfileFields {
2023-03-06 09:30:19 +00:00
return fmt . Errorf ( "cannot have more than %d profile fields" , maximumProfileFields )
}
2023-05-09 10:16:10 +00:00
// Trim each field name + value to maximum allowed length.
for _ , field := range fields {
n := [ ] rune ( field . Name )
if len ( n ) > maximumProfileFieldLength {
field . Name = string ( n [ : maximumProfileFieldLength ] )
}
v := [ ] rune ( field . Value )
if len ( v ) > maximumProfileFieldLength {
field . Value = string ( v [ : maximumProfileFieldLength ] )
}
2023-03-06 09:30:19 +00:00
}
2023-05-09 10:16:10 +00:00
return nil
2023-03-06 09:30:19 +00:00
}
2023-05-25 08:37:38 +00:00
// ListTitle validates the title of a new or updated List.
func ListTitle ( title string ) error {
if title == "" {
return fmt . Errorf ( "list title must be provided, and must be no more than %d chars" , maximumListTitleLength )
}
if length := len ( [ ] rune ( title ) ) ; length > maximumListTitleLength {
return fmt . Errorf ( "list title length must be no more than %d chars, provided title was %d chars" , maximumListTitleLength , length )
}
return nil
}
// ListRepliesPolicy validates the replies_policy of a new or updated list.
func ListRepliesPolicy ( repliesPolicy gtsmodel . RepliesPolicy ) error {
switch repliesPolicy {
case "" , gtsmodel . RepliesPolicyFollowed , gtsmodel . RepliesPolicyList , gtsmodel . RepliesPolicyNone :
// No problem.
return nil
default :
// Uh oh.
return fmt . Errorf ( "list replies_policy must be either empty or one of 'followed', 'list', 'none'" )
}
}
2023-07-29 10:49:14 +00:00
// MarkerName checks that the desired marker timeline name is valid.
func MarkerName ( name string ) error {
if name == "" {
return fmt . Errorf ( "empty string for marker timeline name not allowed" )
}
switch apimodel . MarkerName ( name ) {
case apimodel . MarkerNameHome , apimodel . MarkerNameNotifications :
return nil
}
return fmt . Errorf ( "marker timeline name '%s' was not recognized, valid options are '%s', '%s'" , name , apimodel . MarkerNameHome , apimodel . MarkerNameNotifications )
}
2024-03-06 10:15:58 +00:00
// FilterKeyword validates the title of a new or updated List.
func FilterKeyword ( keyword string ) error {
if keyword == "" {
return fmt . Errorf ( "filter keyword must be provided, and must be no more than %d chars" , maximumFilterKeywordLength )
}
if length := len ( [ ] rune ( keyword ) ) ; length > maximumFilterKeywordLength {
return fmt . Errorf ( "filter keyword length must be no more than %d chars, provided keyword was %d chars" , maximumFilterKeywordLength , length )
}
return nil
}
// FilterContexts validates the context of a new or updated filter.
func FilterContexts ( contexts [ ] apimodel . FilterContext ) error {
if len ( contexts ) == 0 {
return fmt . Errorf ( "at least one filter context is required" )
}
for _ , context := range contexts {
switch context {
case apimodel . FilterContextHome ,
apimodel . FilterContextNotifications ,
apimodel . FilterContextPublic ,
apimodel . FilterContextThread ,
apimodel . FilterContextAccount :
continue
default :
return fmt . Errorf (
"filter context '%s' was not recognized, valid options are '%s', '%s', '%s', '%s', '%s'" ,
context ,
apimodel . FilterContextHome ,
apimodel . FilterContextNotifications ,
apimodel . FilterContextPublic ,
apimodel . FilterContextThread ,
apimodel . FilterContextAccount ,
)
}
}
return nil
}
2024-04-11 09:45:53 +00:00
// CreateAccount checks through all the prerequisites for
// creating a new account, according to the provided form.
// If the account isn't eligible, an error will be returned.
//
// Side effect: normalizes the provided language tag for the user's locale.
func CreateAccount ( form * apimodel . AccountCreateRequest ) error {
if form == nil {
return errors . New ( "form was nil" )
}
if ! config . GetAccountsRegistrationOpen ( ) {
return errors . New ( "registration is not open for this server" )
}
if err := Username ( form . Username ) ; err != nil {
return err
}
if err := Email ( form . Email ) ; err != nil {
return err
}
if err := Password ( form . Password ) ; err != nil {
return err
}
if ! form . Agreement {
return errors . New ( "agreement to terms and conditions not given" )
}
locale , err := Language ( form . Locale )
if err != nil {
return err
}
form . Locale = locale
return SignUpReason ( form . Reason , config . GetAccountsReasonRequired ( ) )
}