Quellcode durchsuchen

Various updates to improve usability and stability.

Bozhin Zafirov vor 2 Wochen
Ursprung
Commit
ddb03fc698
5 geänderte Dateien mit 203 neuen und 89 gelöschten Zeilen
  1. 5 12
      constructors.go
  2. 5 0
      errors.go
  3. 18 18
      forms.go
  4. 60 47
      validate.go
  5. 115 12
      validators.go

+ 5 - 12
constructors.go

@@ -11,22 +11,15 @@ func strFromPtr(str *string) (s string) {
 /* Generate new CharField field with type text */
 func NewCharField(Name string, Value *string) *FormField {
 	return &FormField{
-		Name,
-		nil,
-		"",
-		strFromPtr(Value),
-		"form-control",
-		"text",
-		"",
-		"",
-		false,
-		false,
-		nil,
+		Name:  Name,
+		Value: strFromPtr(Value),
+		Class: "form-control",
+		Type:  "text",
 	}
 }
 
 /* Generate new CharField field with type password */
 func NewPasswordField(Name string) *FormField {
 	field := NewCharField(Name, nil).SetType("password")
-	return &field
+	return field
 }

+ 5 - 0
errors.go

@@ -6,7 +6,12 @@ import (
 
 /* pre-defined form errors */
 var (
+	ENilRequest        = errors.New("NIL request")
+	ENilForm           = errors.New("NIL form")
+	EInvalidForm       = errors.New("Form must be a non-nil pointer to a struct")
 	EInvalidMethod     = errors.New("Invalid method")
 	EInvalidIntValue   = errors.New("Field value must be integer.")
 	EInvalidFloatValue = errors.New("Field value must be float.")
+	ERequiredField     = errors.New("Field is required")
+	EFormHasErrors     = errors.New("Form has errors")
 )

+ 18 - 18
forms.go

@@ -8,7 +8,7 @@ import (
 )
 
 /* ValidatorFunc defines a function for FormField data validation */
-type ValidatorFunc func(FormField, context.Context) error
+type ValidatorFunc func(*FormField, context.Context) error
 
 /* ValidatorsList defines a list of ValidatorFunc */
 type ValidatorsList []ValidatorFunc
@@ -25,59 +25,59 @@ type FormField struct {
 	Help        string
 	Required    bool
 	AutoFocus   bool
-	Validators  *ValidatorsList
+	Validators  ValidatorsList
 }
 
 /* SetValidators configures validators list in form field */
-func (f FormField) SetValidators(validators *ValidatorsList) FormField {
+func (f *FormField) SetValidators(validators ValidatorsList) *FormField {
 	f.Validators = validators
 	return f
 }
 
 /* SetLabel configures form label */
-func (f FormField) SetLabel(label string) FormField {
+func (f *FormField) SetLabel(label string) *FormField {
 	f.Label = label
 	return f
 }
 
 /* SetClass configures class name in form field */
-func (f FormField) SetClass(class string) FormField {
+func (f *FormField) SetClass(class string) *FormField {
 	f.Class = class
 	return f
 }
 
 /* SetRequired marks FormField as mandatory */
-func (f FormField) SetRequired() FormField {
+func (f *FormField) SetRequired() *FormField {
 	f.Required = true
 	return f
 }
 
 /* SetAutoFocus gives focus to current FormField on page load  */
-func (f FormField) SetAutoFocus() FormField {
+func (f *FormField) SetAutoFocus() *FormField {
 	f.AutoFocus = true
 	return f
 }
 
 /* SetType specifies input field type */
-func (f FormField) SetType(t string) FormField {
+func (f *FormField) SetType(t string) *FormField {
 	f.Type = t
 	return f
 }
 
 /* SetPlaceholder specified a placeholder property in Formfield */
-func (f FormField) SetPlaceholder(placeholder string) FormField {
+func (f *FormField) SetPlaceholder(placeholder string) *FormField {
 	f.Placeholder = placeholder
 	return f
 }
 
 /* SetHelp specified a help message for current Formfield */
-func (f FormField) SetHelp(help string) FormField {
+func (f *FormField) SetHelp(help string) *FormField {
 	f.Help = help
 	return f
 }
 
 /* GetString returns FormField.Value as string */
-func (f FormField) GetString() string {
+func (f *FormField) GetString() string {
 	return f.Value
 }
 
@@ -91,7 +91,7 @@ func (f FormField) GetInt() (v int, err error) {
 }
 
 /* Int converts FormField.Value to integer value and ignores errors */
-func (f FormField) Int() int {
+func (f *FormField) Int() int {
 	if result, err := strconv.Atoi(f.Value); err == nil {
 		return result
 	}
@@ -99,12 +99,12 @@ func (f FormField) Int() int {
 }
 
 /* GetFloat returns FormField.Value as float */
-func (f FormField) GetFloat() (float64, error) {
+func (f *FormField) GetFloat() (float64, error) {
 	return strconv.ParseFloat(f.Value, 64)
 }
 
 /* Float converts FormField.Value to float and ignores errors */
-func (f FormField) Float() float64 {
+func (f *FormField) Float() float64 {
 	if result, err := strconv.ParseFloat(f.Value, 64); err == nil {
 		return result
 	}
@@ -112,13 +112,13 @@ func (f FormField) Float() float64 {
 }
 
 /* GetBool returns boolean value for checkbox fields */
-func (f FormField) GetBool() (bool, error) {
+func (f *FormField) GetBool() (bool, error) {
 	/* placeholder */
 	return false, nil
 }
 
 /* GetChecked returns true if checkbox has been checked and its value is "on" */
-func (f FormField) GetChecked() bool {
+func (f *FormField) GetChecked() bool {
 	return f.Value == "on"
 }
 
@@ -127,7 +127,7 @@ const formFieldTemplate = `
 	{{ if .Label }}<label class="form-label" for="{{ .Name }}">{{ .Label }}</label>{{ end }}
 	<input type="{{ .Type }}" id="{{ .Name }}" name="{{ .Name }}"
 		{{- if .Class }} class="{{ .Class }}"{{ end }}
-		{{- if .Value }} value="{{ .Value }}"{{ end }}
+		{{- if and .Value (ne .Type "password") }} value="{{ .Value }}"{{ end }}
 		{{- if .Placeholder}} placeholder="{{ .Placeholder }}"{{ end }}
 		{{- if .Help }} aria-describedby="{{ .Name }}Help"{{ end }}
 		{{- if .Required }} required{{ end }}
@@ -140,7 +140,7 @@ const formFieldTemplate = `
 var formTemplate = template.Must(template.New("FormField").Parse(formFieldTemplate))
 
 /* HTML renders FormField element in html format */
-func (f FormField) HTML() template.HTML {
+func (f *FormField) HTML() template.HTML {
 	var buffer bytes.Buffer
 	if err := formTemplate.Execute(&buffer, f); err == nil {
 		return template.HTML(buffer.String())

+ 60 - 47
validate.go

@@ -5,62 +5,75 @@ import (
 	"reflect"
 )
 
-/* ValidateForm parses a POST form into pre-defined struct */
-func ValidateForm(r *http.Request, p interface{}) error {
-	/* only support POST methods */
-	if r.Method != "POST" {
+/* ValidateForm parses a POST form into a pre-defined struct */
+func ValidateForm(r *http.Request, p any) (formErr error) {
+	/* asserts */
+	switch {
+	case r == nil:
+		/* error on nil requestst */
+		return ENilRequest
+	case r.Method != http.MethodPost:
+		/* only support with POST methods */
 		return EInvalidMethod
+	case p == nil:
+		/* error on nil form */
+		return ENilForm
 	}
-
-	/* parse POST data into form */
+	/* assert on form being a pointer */
+	fv := reflect.ValueOf(p)
+	if fv.Kind() != reflect.Pointer || fv.IsNil() {
+		return EInvalidForm
+	}
+	/* assert on form being a pointer to a struct */
+	fv = fv.Elem()
+	if fv.Kind() != reflect.Struct {
+		return EInvalidForm
+	}
+	/* parse form in the request */
 	if err := r.ParseForm(); err != nil {
+		/* TODO: handle multipart forms / file uploads */
 		return err
 	}
-
-	var FormError error
-	/* Parse form data into interface */
-	formStruct := reflect.ValueOf(p).Elem()
-
-	/* populate FormField value */
-	for HttpFormField, HttpFormValue := range r.Form {
-		for n := 0; n < formStruct.NumField(); n++ {
-			fieldt := formStruct.Type().Field(n)
-			/* get n-th field */
-			fieldn := formStruct.Field(n)
-			/* only proceed if field name or tag matches that of form field */
-			if fieldn.Field(0).String() != HttpFormField {
-				if fieldt.Name != HttpFormField {
-					if fieldt.Tag.Get("form") != HttpFormField {
-						continue
-					}
-				}
-			}
-			/* set form data to field
-			   equivalent of form.Value = HttpFormValue[0] */
-			fieldn.Field(2).Set(reflect.ValueOf(HttpFormValue[0]))
+	/* walk target form fields */
+	for idx := 0; idx < fv.NumField(); idx++ {
+		f := fv.Field(idx)
+		/* ignore non-pointer form fields or nil fields */
+		if f.Kind() != reflect.Pointer || f.IsNil() {
+			continue
 		}
-	}
-
-	/* run form field validators */
-	for n := 0; n < formStruct.NumField(); n++ {
-		fieldn := formStruct.Field(n)
-		field := fieldn.Interface().(FormField)
-		if field.Validators == nil {
+		/* ignore fields of type different from *FormField */
+		ff, ok := f.Interface().(*FormField)
+		if !ok || ff == nil {
 			continue
 		}
-		/* prepare list of FormField errors */
-		var errors []error
-		for _, validator := range *field.Validators {
-			if err := validator(field, r.Context()); err != nil {
-				errors = append(errors, err)
-				FormError = err
-			}
+		ff.Error = nil
+		/* store value in *FormField */
+		if v, ok := r.PostForm[ff.Name]; ok {
+			ff.Value = v[0]
+		} else {
+			ff.Value = ""
+		}
+		/* assert on required fields */
+		if ff.Required && ff.Value == "" {
+			ff.Error = append(ff.Error, ERequiredField)
+			formErr = EFormHasErrors
+			continue
+		}
+		/* do not run validators on empty values */
+		if ff.Value == "" {
+			continue
 		}
-		if len(errors) != 0 {
-			fieldn.Field(1).Set(reflect.ValueOf(errors))
+		/* run field validators if any */
+		for _, fieldValidator := range ff.Validators {
+			if fieldValidator == nil {
+				continue
+			}
+			if err := fieldValidator(ff, r.Context()); err != nil {
+				ff.Error = append(ff.Error, err)
+				formErr = EFormHasErrors
+				break
+			}
 		}
 	}
-
-	/* return status */
-	return FormError
+	return
 }

+ 115 - 12
validators.go

@@ -4,19 +4,56 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/mail"
 	"strings"
+	"unicode/utf8"
 )
 
 /* validation errors */
 var (
-	EInvalidInteger = errors.New("not a valid integer value")
-	EInvalidFloat   = errors.New("not a valid float value")
-	ERequired       = errors.New("this field is required")
+	EInvalidInteger   = errors.New("not a valid integer value")
+	EInvalidFloat     = errors.New("not a valid float value")
+	ERequired         = errors.New("this field is required")
+	EInvalidEmail     = errors.New("invalid email address")
+	EInvalidDomain    = errors.New("invalid domain name")
+	EInvalidDomainTLD = errors.New("invalid domain TLD")
 )
 
+/* A globaly defined set with valid domain TLDs */
+var validTLDs = map[string]struct{}{
+	/* general purpose domain names */
+	"aero": {}, "asia": {}, "biz": {}, "cat": {}, "com": {}, "coop": {}, "info": {}, "int": {}, "jobs": {},
+	"mobi": {}, "museum": {}, "name": {}, "net": {}, "org": {}, "pro": {}, "tel": {}, "travel": {}, "xxx": {},
+	"edu": {}, "gov": {}, "mil": {},
+	/* country code domain names */
+	"ac": {}, "ad": {}, "ae": {}, "af": {}, "ag": {}, "ai": {}, "al": {}, "am": {}, "an": {}, "ao": {}, "aq": {},
+	"ar": {}, "as": {}, "at": {}, "au": {}, "aw": {}, "ax": {}, "az": {}, "ba": {}, "bb": {}, "bd": {}, "be": {},
+	"bf": {}, "bg": {}, "bh": {}, "bi": {}, "bj": {}, "bm": {}, "bn": {}, "bo": {}, "br": {}, "bs": {}, "bt": {},
+	"bv": {}, "bw": {}, "by": {}, "bz": {}, "ca": {}, "cc": {}, "cd": {}, "cf": {}, "cg": {}, "ch": {}, "ci": {},
+	"ck": {}, "cl": {}, "cm": {}, "cn": {}, "co": {}, "cr": {}, "cs": {}, "cu": {}, "cv": {}, "cx": {}, "cy": {},
+	"cz": {}, "dd": {}, "de": {}, "dj": {}, "dk": {}, "dm": {}, "do": {}, "dz": {}, "ec": {}, "ee": {}, "eg": {},
+	"eh": {}, "er": {}, "es": {}, "et": {}, "eu": {}, "fi": {}, "fj": {}, "fk": {}, "fm": {}, "fo": {}, "fr": {},
+	"ga": {}, "gb": {}, "gd": {}, "ge": {}, "gf": {}, "gg": {}, "gh": {}, "gi": {}, "gl": {}, "gm": {}, "gn": {},
+	"gp": {}, "gq": {}, "gr": {}, "gs": {}, "gt": {}, "gu": {}, "gw": {}, "gy": {}, "hk": {}, "hm": {}, "hn": {},
+	"hr": {}, "ht": {}, "hu": {}, "id": {}, "ie": {}, "il": {}, "im": {}, "in": {}, "io": {}, "iq": {}, "ir": {},
+	"is": {}, "it": {}, "je": {}, "jm": {}, "jo": {}, "jp": {}, "ke": {}, "kg": {}, "kh": {}, "ki": {}, "km": {},
+	"kn": {}, "kp": {}, "kr": {}, "kw": {}, "ky": {}, "kz": {}, "la": {}, "lb": {}, "lc": {}, "li": {}, "lk": {},
+	"lr": {}, "ls": {}, "lt": {}, "lu": {}, "lv": {}, "ly": {}, "ma": {}, "mc": {}, "md": {}, "me": {}, "mg": {},
+	"mh": {}, "mk": {}, "ml": {}, "mm": {}, "mn": {}, "mo": {}, "mp": {}, "mq": {}, "mr": {}, "ms": {}, "mt": {},
+	"mu": {}, "mv": {}, "mw": {}, "mx": {}, "my": {}, "mz": {}, "na": {}, "nc": {}, "ne": {}, "nf": {}, "ng": {},
+	"ni": {}, "nl": {}, "no": {}, "np": {}, "nr": {}, "nu": {}, "nz": {}, "om": {}, "pa": {}, "pe": {}, "pf": {},
+	"pg": {}, "ph": {}, "pk": {}, "pl": {}, "pm": {}, "pn": {}, "pr": {}, "ps": {}, "pt": {}, "pw": {}, "py": {},
+	"qa": {}, "re": {}, "ro": {}, "rs": {}, "ru": {}, "rw": {}, "sa": {}, "sb": {}, "sc": {}, "sd": {}, "se": {},
+	"sg": {}, "sh": {}, "si": {}, "sj": {}, "sk": {}, "sl": {}, "sm": {}, "sn": {}, "so": {}, "sr": {}, "ss": {},
+	"st": {}, "su": {}, "sv": {}, "sy": {}, "sz": {}, "tc": {}, "td": {}, "tf": {}, "tg": {}, "th": {}, "tj": {},
+	"tk": {}, "tl": {}, "tm": {}, "tn": {}, "to": {}, "tp": {}, "tr": {}, "tt": {}, "tv": {}, "tw": {}, "tz": {},
+	"ua": {}, "ug": {}, "uk": {}, "us": {}, "uy": {}, "uz": {}, "va": {}, "vc": {}, "ve": {}, "vg": {}, "vi": {},
+	"vn": {}, "vu": {}, "wf": {}, "ws": {}, "ye": {}, "yt": {}, "yu": {}, "za": {}, "zm": {}, "zw": {},
+}
+
 /* ValidLettersGeneric is a validator generator for checking for valid letters in field */
 func ValidLettersGeneric(Letters string, Error error) ValidatorFunc {
-	Callback := func(field FormField, ctx context.Context) error {
+	Callback := func(field *FormField, ctx context.Context) error {
 		for _, Rune := range field.GetString() {
 			if strings.IndexRune(Letters, Rune) == -1 {
 				return Error
@@ -28,7 +65,7 @@ func ValidLettersGeneric(Letters string, Error error) ValidatorFunc {
 }
 
 /* ValidRequired makes sure field is not empty. */
-func ValidRequired(field FormField, ctx context.Context) error {
+func ValidRequired(field *FormField, ctx context.Context) error {
 	if field.GetString() == "" {
 		return ERequired
 	}
@@ -39,7 +76,7 @@ func ValidRequired(field FormField, ctx context.Context) error {
 func ValidLength(min, max int) ValidatorFunc {
 	var ELength = errors.New(
 		fmt.Sprintf("must be a string between %d and %d characters in length", min, max))
-	return func(field FormField, ctx context.Context) error {
+	return func(field *FormField, ctx context.Context) error {
 		if len(field.GetString()) != 0 && (len(field.GetString()) < min || len(field.GetString()) > max) {
 			return ELength
 		}
@@ -55,7 +92,7 @@ func ValidFieldIn(list []string) ValidatorFunc {
 			strings.Join(list, ","),
 		),
 	)
-	return func(field FormField, ctx context.Context) error {
+	return func(field *FormField, ctx context.Context) error {
 		for _, item := range list {
 			if item == field.GetString() {
 				return nil
@@ -66,7 +103,7 @@ func ValidFieldIn(list []string) ValidatorFunc {
 }
 
 /* ValidInt returns error if field does not contain a valid integer value */
-func ValidInt(field FormField, ctx context.Context) error {
+func ValidInt(field *FormField, ctx context.Context) error {
 	_, err := field.GetInt()
 	if err != nil {
 		return EInvalidInteger
@@ -78,7 +115,7 @@ func ValidInt(field FormField, ctx context.Context) error {
 func ValidBetween(min, max int) ValidatorFunc {
 	var EInvalidInterval = errors.New(
 		fmt.Sprintf("must be integer between %d and %d", min, max))
-	return func(field FormField, ctx context.Context) error {
+	return func(field *FormField, ctx context.Context) error {
 		value, err := field.GetInt()
 		if err != nil {
 			return EInvalidInteger
@@ -91,7 +128,7 @@ func ValidBetween(min, max int) ValidatorFunc {
 }
 
 /* ValidFloat returns error if field does not contain a valid integer value */
-func ValidFloat(field FormField, ctx context.Context) error {
+func ValidFloat(field *FormField, ctx context.Context) error {
 	_, err := field.GetFloat()
 	if err != nil {
 		return EInvalidFloat
@@ -103,7 +140,7 @@ func ValidFloat(field FormField, ctx context.Context) error {
 func ValidBetweenFloat(min, max float64) ValidatorFunc {
 	var EInvalidInterval = errors.New(
 		fmt.Sprintf("must be float value between %.2f and %.2f", min, max))
-	return func(field FormField, ctx context.Context) error {
+	return func(field *FormField, ctx context.Context) error {
 		value, err := field.GetFloat()
 		if err != nil {
 			return err
@@ -117,10 +154,76 @@ func ValidBetweenFloat(min, max float64) ValidatorFunc {
 
 /* ValidFieldEqualTo is a validator that checks if two fields have the same value. */
 func ValidFieldEqualTo(Other *FormField, err error) ValidatorFunc {
-	return func(field FormField, ctx context.Context) error {
+	return func(field *FormField, ctx context.Context) error {
 		if field.GetString() != Other.GetString() {
 			return err
 		}
 		return nil
 	}
 }
+
+/* validDomainNameStr performs checks if the domain name stored in *FormField is valid */
+func validDomainNameStr(domain string) error {
+	domain = strings.ToLower(strings.TrimSpace(domain))
+	domainLen := len(domain)
+	/* common domain name checks */
+	if domainLen == 0 || domainLen > 253 || strings.ContainsAny(domain, " \t\r\n") {
+		return EInvalidDomain
+	}
+	if domain[0] == '.' || domain[domainLen-1] == '.' {
+		return EInvalidDomain
+	}
+	/* make sure domain name only contains allowed letters */
+	for _, r := range domain {
+		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
+			continue
+		}
+		return EInvalidDomain
+	}
+	/* split to levels (tld, domain[, subdomain...] */
+	levels := strings.Split(domain, ".")
+	levelsLen := len(levels)
+	if levelsLen < 2 {
+		return EInvalidDomain
+	}
+	/* perform common checks on levels */
+	for _, level := range levels {
+		levelLen := len(level)
+		if levelLen == 0 || levelLen > 63 {
+			return EInvalidDomain
+		}
+		if level[0] == '-' || level[levelLen-1] == '-' {
+			return EInvalidDomain
+		}
+
+	}
+	/* make sure tld has proper size and is whitelisted */
+	domainTLD := levels[len(levels)-1]
+	if len(domainTLD) < 2 {
+		return EInvalidDomainTLD
+	}
+	if _, ok := validTLDs[domainTLD]; !ok {
+		return EInvalidDomainTLD
+	}
+	return nil
+}
+
+/* ValidEmail checks if field contains a valid email address */
+func ValidEmail(field *FormField, ctx context.Context) error {
+	/* sanitize input */
+	addrStr := strings.TrimSpace(field.GetString())
+	if addrStr == "" || !utf8.ValidString(addrStr) || strings.ContainsAny(addrStr, " \t\r\n") {
+		return EInvalidEmail
+	}
+	/* use mail.ParseAddress on the sanitized text */
+	addr, err := mail.ParseAddress(addrStr)
+	if err != nil || addr.Address != addrStr {
+		return EInvalidEmail
+	}
+	/* make sure email address has a valid domain name */
+	parts := strings.Split(addrStr, "@")
+	if err := validDomainNameStr(parts[len(parts)-1]); err != nil {
+		return EInvalidEmail
+	}
+	return nil
+}