Bläddra i källkod

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 månader sedan
förälder
incheckning
35d1de6879
4 ändrade filer med 116 tillägg och 48 borttagningar
  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 */
 func NewCsrfField(w http.ResponseWriter, r *http.Request, secure bool) *FormField {
 	return &FormField{

+ 14 - 3
fields.html

@@ -1,6 +1,6 @@
 {{define "checkbox"}}
 <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}}
 </div>
 {{if .Help}}  <div id="{{.Name}}Help" class="form-text">{{.Help}}</div>{{end}}
@@ -9,7 +9,7 @@
 
 {{define "password"}}
 {{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 .Help}} aria-describedby="{{.Name}}Help"{{end}}
     {{- if .Required}} required{{end}}
@@ -20,17 +20,28 @@
 
 {{define "text"}}
 {{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 .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
 	{{- if .Help}} aria-describedby="{{.Name}}Help"{{end}}
 	{{- if .AutoComplete}} autocomplete="{{.AutoComplete}}"{{end}}
     {{- if .Required}} required{{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 .Error}}{{range $e := .Error}}<div class="text-danger">{{$e}}</div>{{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"}}
 <input type="hidden" id="{{.Name}}" name="{{.Name}}" value="{{.Value}}">
 {{end}}

+ 12 - 4
forms.go

@@ -21,6 +21,7 @@ type FormField struct {
 	Value        string
 	Label        string
 	Kind         string
+	Select       []string
 	Placeholder  string
 	Help         string
 	Required     bool
@@ -28,6 +29,7 @@ type FormField struct {
 	Sticky       bool
 	Checked      bool
 	AutoComplete string
+	Suffix       string
 	Validators   ValidatorsList
 }
 
@@ -96,6 +98,12 @@ func (f *FormField) SetAutoComplete(on bool) *FormField {
 	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 */
 func (f *FormField) GetString() string {
 	return f.Value
@@ -110,8 +118,8 @@ func (f FormField) GetInt() (v int, err error) {
 	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 {
 		return result
 	}
@@ -123,8 +131,8 @@ 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 {
+/* SafeFloat converts FormField.Value to float and ignores errors */
+func (f *FormField) SafeFloat() float64 {
 	if result, err := strconv.ParseFloat(f.Value, 64); err == nil {
 		return result
 	}

+ 80 - 41
validators.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net"
 	"net/http"
 	"net/mail"
 	"strings"
@@ -74,20 +75,15 @@ func ValidLength(vMin, vMax int, err error) ValidatorFunc {
 }
 
 /* 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 {
 		for _, item := range list {
 			if item == field.GetString() {
 				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. */
-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 {
-		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
 	}
@@ -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 == '-' {
 				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 */
 		for _, level := range levels {
 			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] == '-' {
-				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
 	}
 }
 
+/* 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 */
 func ValidDomainName(err error) ValidatorFunc {
-	callbackCheck := validDomainNameStr(err)
+	callbackCheck := validHostnameStr(2, err)
 	/* domain name checks */
 	return func(field *FormField, ctx context.Context) error {
 		return callbackCheck(field.GetString())
@@ -234,7 +240,7 @@ func ValidDomainNameTLD(err error) ValidatorFunc {
 /* ValidEmail checks if field contains a valid email address */
 func ValidEmail(err error) ValidatorFunc {
 	EInvalidEmail := errors.New("invalid email address")
-	domainValidator := validDomainNameStr(nil)
+	domainValidator := validHostnameStr(2, nil)
 	return func(field *FormField, ctx context.Context) error {
 		/* sanitize input */
 		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 */
 func ValidCSRF(r *http.Request) ValidatorFunc {
 	return func(field *FormField, ctx context.Context) error {