Просмотр исходного кода

Various changes:

    1. Add Select field type
    2. Highlight fields with errors
    3. Add suffix capability for input fields
    4. Rename Int() and Float() to SafeInt() and SafeFloat()
    5. Add more error handling in validators
    6. Add validators for IP addresses
    7. Change domain validation to fit hostname validation
Bozhin Zafirov 6 месяцев назад
Родитель
Сommit
35d1de6879
4 измененных файлов с 116 добавлено и 48 удалено
  1. 10 0
      constructors.go
  2. 14 3
      fields.html
  3. 12 4
      forms.go
  4. 80 41
      validators.go

+ 10 - 0
constructors.go

@@ -50,6 +50,16 @@ func NewCheckboxField(name string, checked bool) *FormField {
 	}
 	}
 }
 }
 
 
+/* Generate new select field */
+func NewSelectField(name string, items []string, value string) *FormField {
+	return &FormField{
+		Name:   name,
+		Select: items,
+		Value:  value,
+		Kind:   "select",
+	}
+}
+
 /* Generate a new CSRF field */
 /* Generate a new CSRF field */
 func NewCsrfField(w http.ResponseWriter, r *http.Request, secure bool) *FormField {
 func NewCsrfField(w http.ResponseWriter, r *http.Request, secure bool) *FormField {
 	return &FormField{
 	return &FormField{

+ 14 - 3
fields.html

@@ -1,6 +1,6 @@
 {{define "checkbox"}}
 {{define "checkbox"}}
 <div class="form-check">
 <div class="form-check">
-  <input type="checkbox" class="form-check-input" id="{{.Name}}" name="{{.Name}}"{{if .Required}} required{{end}}{{if .Checked}} checked{{end}}>
+  <input type="checkbox" class="form-check-input{{if .Error}} is-invalid{{end}}" id="{{.Name}}" name="{{.Name}}"{{if .Required}} required{{end}}{{if .Checked}} checked{{end}}>
 {{if .Label}}  <label class="form-check-label" for="{{.Name}}">{{.Label}}</label>{{end}}
 {{if .Label}}  <label class="form-check-label" for="{{.Name}}">{{.Label}}</label>{{end}}
 </div>
 </div>
 {{if .Help}}  <div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
 {{if .Help}}  <div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
@@ -9,7 +9,7 @@
 
 
 {{define "password"}}
 {{define "password"}}
 {{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
 {{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
-<input type="password" id="{{.Name}}" name="{{.Name}}" class="form-control"
+<input type="password" id="{{.Name}}" name="{{.Name}}" class="form-control{{if .Error}} is-invalid{{end}}"
 	{{- if .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
 	{{- if .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
 	{{- if .Help}} aria-describedby="{{.Name}}Help"{{end}}
 	{{- if .Help}} aria-describedby="{{.Name}}Help"{{end}}
     {{- if .Required}} required{{end}}
     {{- if .Required}} required{{end}}
@@ -20,17 +20,28 @@
 
 
 {{define "text"}}
 {{define "text"}}
 {{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
 {{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
-<input type="text" id="{{.Name}}" name="{{.Name}}" class="form-control"
+<div class="input-group mb-3">
+<input type="text" id="{{.Name}}" name="{{.Name}}" class="form-control{{if .Error}} is-invalid{{end}}"
 	{{- if .Value}} value="{{.Value}}"{{end}}
 	{{- if .Value}} value="{{.Value}}"{{end}}
 	{{- if .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
 	{{- if .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
 	{{- if .Help}} aria-describedby="{{.Name}}Help"{{end}}
 	{{- if .Help}} aria-describedby="{{.Name}}Help"{{end}}
 	{{- if .AutoComplete}} autocomplete="{{.AutoComplete}}"{{end}}
 	{{- if .AutoComplete}} autocomplete="{{.AutoComplete}}"{{end}}
     {{- if .Required}} required{{end}}
     {{- if .Required}} required{{end}}
     {{- if .AutoFocus}} autofocus{{end}}>
     {{- if .AutoFocus}} autofocus{{end}}>
+	{{if .Suffix}}<span class="input-group-text" id="{{.Name}}-group"><b>{{.Suffix}}</b></span>{{end}}
+</div>
     {{if .Help}}<div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
     {{if .Help}}<div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
 {{if .Error}}{{range $e := .Error}}<div class="text-danger">{{$e}}</div>{{end}}{{end}}
 {{if .Error}}{{range $e := .Error}}<div class="text-danger">{{$e}}</div>{{end}}{{end}}
 {{end}}
 {{end}}
 
 
+{{define "select"}}
+{{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
+<select id="{{.Name}}" name="{{.Name}}" class="form-select{{if .Error}} is-invalid{{end}}">
+{{range $item := .Select}}<option value="{{$item}}"{{if eq $.Value $item}} selected{{end}}>{{$item}}</option>{{end}}
+</select>
+{{if .Help}}<div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
+{{end}}
+
 {{define "hidden"}}
 {{define "hidden"}}
 <input type="hidden" id="{{.Name}}" name="{{.Name}}" value="{{.Value}}">
 <input type="hidden" id="{{.Name}}" name="{{.Name}}" value="{{.Value}}">
 {{end}}
 {{end}}

+ 12 - 4
forms.go

@@ -21,6 +21,7 @@ type FormField struct {
 	Value        string
 	Value        string
 	Label        string
 	Label        string
 	Kind         string
 	Kind         string
+	Select       []string
 	Placeholder  string
 	Placeholder  string
 	Help         string
 	Help         string
 	Required     bool
 	Required     bool
@@ -28,6 +29,7 @@ type FormField struct {
 	Sticky       bool
 	Sticky       bool
 	Checked      bool
 	Checked      bool
 	AutoComplete string
 	AutoComplete string
+	Suffix       string
 	Validators   ValidatorsList
 	Validators   ValidatorsList
 }
 }
 
 
@@ -96,6 +98,12 @@ func (f *FormField) SetAutoComplete(on bool) *FormField {
 	return f
 	return f
 }
 }
 
 
+/* SetSuffix configures a suffix for text input field */
+func (f *FormField) SetSuffix(suffix string) *FormField {
+	f.Suffix = suffix
+	return f
+}
+
 /* GetString returns FormField.Value as string */
 /* GetString returns FormField.Value as string */
 func (f *FormField) GetString() string {
 func (f *FormField) GetString() string {
 	return f.Value
 	return f.Value
@@ -110,8 +118,8 @@ func (f FormField) GetInt() (v int, err error) {
 	return
 	return
 }
 }
 
 
-/* Int converts FormField.Value to integer value and ignores errors */
-func (f *FormField) Int() int {
+/* SafeInt converts FormField.Value to integer value and ignores errors */
+func (f *FormField) SafeInt() int {
 	if result, err := strconv.Atoi(f.Value); err == nil {
 	if result, err := strconv.Atoi(f.Value); err == nil {
 		return result
 		return result
 	}
 	}
@@ -123,8 +131,8 @@ func (f *FormField) GetFloat() (float64, error) {
 	return strconv.ParseFloat(f.Value, 64)
 	return strconv.ParseFloat(f.Value, 64)
 }
 }
 
 
-/* Float converts FormField.Value to float and ignores errors */
-func (f *FormField) Float() float64 {
+/* SafeFloat converts FormField.Value to float and ignores errors */
+func (f *FormField) SafeFloat() float64 {
 	if result, err := strconv.ParseFloat(f.Value, 64); err == nil {
 	if result, err := strconv.ParseFloat(f.Value, 64); err == nil {
 		return result
 		return result
 	}
 	}

+ 80 - 41
validators.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"net"
 	"net/http"
 	"net/http"
 	"net/mail"
 	"net/mail"
 	"strings"
 	"strings"
@@ -74,20 +75,15 @@ func ValidLength(vMin, vMax int, err error) ValidatorFunc {
 }
 }
 
 
 /* ValidFieldIn verifies if item is within the list of items */
 /* ValidFieldIn verifies if item is within the list of items */
-func ValidFieldIn(list []string) ValidatorFunc {
-	var EInvalidValue = errors.New(
-		fmt.Sprintf(
-			"field value must be one of: %s",
-			strings.Join(list, ","),
-		),
-	)
+func ValidFieldIn(list []string, err error) ValidatorFunc {
+	EInvalidValue := fmt.Errorf("field value must be one of: %s", strings.Join(list, ","))
 	return func(field *FormField, ctx context.Context) error {
 	return func(field *FormField, ctx context.Context) error {
 		for _, item := range list {
 		for _, item := range list {
 			if item == field.GetString() {
 			if item == field.GetString() {
 				return nil
 				return nil
 			}
 			}
 		}
 		}
-		return EInvalidValue
+		return fallbackError(err, EInvalidValue)
 	}
 	}
 }
 }
 
 
@@ -101,16 +97,11 @@ func ValidInt(field *FormField, ctx context.Context) error {
 }
 }
 
 
 /* ValidBetween makes sure that field is integer value within the specified range. */
 /* ValidBetween makes sure that field is integer value within the specified range. */
-func ValidBetween(min, max int) ValidatorFunc {
-	var EInvalidInterval = errors.New(
-		fmt.Sprintf("must be integer between %d and %d", min, max))
+func ValidBetween(minV, maxV int, err error) ValidatorFunc {
+	var EInvalidInterval = fmt.Errorf("must be integer between %d and %d", minV, maxV)
 	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
-		}
-		if value < min || value > max {
-			return EInvalidInterval
+		if value, err := field.GetInt(); err != nil || (value < minV || value > maxV) {
+			return fallbackError(err, EInvalidInterval)
 		}
 		}
 		return nil
 		return nil
 	}
 	}
@@ -163,50 +154,65 @@ func ValidExact(text string, err error) ValidatorFunc {
 	}
 	}
 }
 }
 
 
-/* validDomainNameStr performs checks if the domain name stored in *FormField is valid */
-func validDomainNameStr(err error) func(string) error {
-	EInvalidDomain := errors.New("invalid domain name")
-	return func(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 fallbackError(err, EInvalidDomain)
+/* validHostnameStr performs checks if the hostname name stored in *FormField is valid */
+func validHostnameStr(minLevel int, err error) func(string) error {
+	EInvalidHostname := errors.New("invalid hostname")
+	return func(hostname string) error {
+		hostname = strings.ToLower(strings.TrimSpace(hostname))
+		hostnameLen := len(hostname)
+		/* common hostname name checks */
+		if hostnameLen == 0 || hostnameLen > 253 || strings.ContainsAny(hostname, " \t\r\n") {
+			return fallbackError(err, EInvalidHostname)
 		}
 		}
-		if domain[0] == '.' || domain[domainLen-1] == '.' {
-			return fallbackError(err, EInvalidDomain)
+		if hostname[0] == '.' {
+			return fallbackError(err, EInvalidHostname)
 		}
 		}
-		/* make sure domain name only contains allowed letters */
-		for _, r := range domain {
+		/* make sure hostname name only contains allowed letters */
+		for _, r := range hostname {
 			if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
 			if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
 				continue
 				continue
 			}
 			}
-			return fallbackError(err, EInvalidDomain)
+			return fallbackError(err, EInvalidHostname)
 		}
 		}
-		/* split to levels (tld, domain[, subdomain...] */
-		levels := strings.Split(domain, ".")
-		levelsLen := len(levels)
-		if levelsLen < 2 {
-			return fallbackError(err, EInvalidDomain)
+		/* split to levels (tld, hostname[, subhostname...] */
+		levels := strings.Split(hostname, ".")
+		if len(levels) < minLevel {
+			return fallbackError(err, EInvalidHostname)
 		}
 		}
 		/* perform common checks on levels */
 		/* perform common checks on levels */
 		for _, level := range levels {
 		for _, level := range levels {
 			levelLen := len(level)
 			levelLen := len(level)
-			if levelLen == 0 || levelLen > 63 {
-				return fallbackError(err, EInvalidDomain)
+			if levelLen == 0 {
+				continue
+			}
+			if levelLen > 63 {
+				return fallbackError(err, EInvalidHostname)
 			}
 			}
 			if level[0] == '-' || level[levelLen-1] == '-' {
 			if level[0] == '-' || level[levelLen-1] == '-' {
-				return fallbackError(err, EInvalidDomain)
+				return fallbackError(err, EInvalidHostname)
 			}
 			}
 
 
 		}
 		}
+		/* make sure hostname is not an IP address */
+		if ip := net.ParseIP(hostname); ip != nil {
+			return fallbackError(err, EInvalidHostname)
+		}
 		return nil
 		return nil
 	}
 	}
 }
 }
 
 
+/* ValidHostname performs basic hostname checks */
+func ValidHostname(err error) ValidatorFunc {
+	callbackCheck := validHostnameStr(0, err)
+	/* hostname name checks */
+	return func(field *FormField, ctx context.Context) error {
+		return callbackCheck(field.GetString())
+	}
+}
+
 /* ValidDomainName performs basic domain name checks */
 /* ValidDomainName performs basic domain name checks */
 func ValidDomainName(err error) ValidatorFunc {
 func ValidDomainName(err error) ValidatorFunc {
-	callbackCheck := validDomainNameStr(err)
+	callbackCheck := validHostnameStr(2, err)
 	/* domain name checks */
 	/* domain name checks */
 	return func(field *FormField, ctx context.Context) error {
 	return func(field *FormField, ctx context.Context) error {
 		return callbackCheck(field.GetString())
 		return callbackCheck(field.GetString())
@@ -234,7 +240,7 @@ func ValidDomainNameTLD(err error) ValidatorFunc {
 /* ValidEmail checks if field contains a valid email address */
 /* ValidEmail checks if field contains a valid email address */
 func ValidEmail(err error) ValidatorFunc {
 func ValidEmail(err error) ValidatorFunc {
 	EInvalidEmail := errors.New("invalid email address")
 	EInvalidEmail := errors.New("invalid email address")
-	domainValidator := validDomainNameStr(nil)
+	domainValidator := validHostnameStr(2, nil)
 	return func(field *FormField, ctx context.Context) error {
 	return func(field *FormField, ctx context.Context) error {
 		/* sanitize input */
 		/* sanitize input */
 		addrStr := strings.TrimSpace(field.GetString())
 		addrStr := strings.TrimSpace(field.GetString())
@@ -255,6 +261,39 @@ func ValidEmail(err error) ValidatorFunc {
 	}
 	}
 }
 }
 
 
+/* ValidIP checks if field contains a valid network address */
+func ValidIP(err error) ValidatorFunc {
+	EInvalidAddress := errors.New("invalid network address")
+	return func(field *FormField, ctx context.Context) error {
+		if ip := net.ParseIP(field.GetString()); ip == nil {
+			return fallbackError(err, EInvalidAddress)
+		}
+		return nil
+	}
+}
+
+/* ValidIPv4 checks if field contains a valid IPv4 network address */
+func ValidIPv4(err error) ValidatorFunc {
+	EInvalidAddress := errors.New("invalid ipv4 address")
+	return func(field *FormField, ctx context.Context) error {
+		if ip := net.ParseIP(field.GetString()); ip == nil || ip.To4() == nil {
+			return fallbackError(err, EInvalidAddress)
+		}
+		return nil
+	}
+}
+
+/* ValidIPv6 checks if field contains a valid IPv6 network address */
+func ValidIPv6(err error) ValidatorFunc {
+	EInvalidAddress := errors.New("invalid ipv6 address")
+	return func(field *FormField, ctx context.Context) error {
+		if ip := net.ParseIP(field.GetString()); ip == nil || ip.To4() != nil {
+			return fallbackError(err, EInvalidAddress)
+		}
+		return nil
+	}
+}
+
 /* ValidCSRF checks if CSRF token is valid */
 /* ValidCSRF checks if CSRF token is valid */
 func ValidCSRF(r *http.Request) ValidatorFunc {
 func ValidCSRF(r *http.Request) ValidatorFunc {
 	return func(field *FormField, ctx context.Context) error {
 	return func(field *FormField, ctx context.Context) error {