gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }