4 Komitmen 2c1a1ef523 ... 0822709f4b

Pembuat SHA1 Pesan Tanggal
  Bozhin Zafirov 0822709f4b Add validator that checks local part of email address. 5 bulan lalu
  Bozhin Zafirov 35d1de6879 Various changes: 7 bulan lalu
  Bozhin Zafirov 842bea0386 Various changes: 8 bulan lalu
  Bozhin Zafirov 6048bf0b53 Various changes: 9 bulan lalu
7 mengubah file dengan 340 tambahan dan 156 penghapusan
  1. 50 18
      constructors.go
  2. 9 0
      fallbackerror.go
  3. 47 0
      fields.html
  4. 55 43
      forms.go
  5. 0 10
      templates/field.html
  6. 2 2
      validate.go
  7. 177 83
      validators.go

+ 50 - 18
constructors.go

@@ -1,6 +1,9 @@
 package forms
 
-import "net/http"
+import (
+	"net/http"
+	"strconv"
+)
 
 /* strFromPtr converts string pointer to a string */
 func strFromPtr(str *string) (s string) {
@@ -11,22 +14,49 @@ func strFromPtr(str *string) (s string) {
 }
 
 /* Generate new CharField field with type text */
-func NewCharField(Name string, Value *string) *FormField {
+func NewCharField(name string, value *string) *FormField {
 	return &FormField{
-		Name:  Name,
-		Value: strFromPtr(Value),
-		Class: "form-control",
-		Type:  "text",
+		Name:  name,
+		Value: strFromPtr(value),
+		Kind:  "text",
 	}
 }
 
-/* Generate a new hidden text field */
-func NewHiddenField(Name string, Value *string) *FormField {
+/* Generate new integer type field */
+func NewIntField(name string, value int) *FormField {
+	field := FormField{
+		Name:  name,
+		Value: strconv.Itoa(value),
+		Kind:  "text",
+	}
+	field.AddValidator(ValidInt)
+	return &field
+}
+
+/* Generate new password field */
+func NewPasswordField(name string) *FormField {
+	return &FormField{
+		Name: name,
+		Kind: "password",
+	}
+}
+
+/* Generate a new checkbox field */
+func NewCheckboxField(name string, checked bool) *FormField {
 	return &FormField{
-		Name:  Name,
-		Value: strFromPtr(Value),
-		Class: "form-control",
-		Type:  "hidden",
+		Name:    name,
+		Checked: checked,
+		Kind:    "checkbox",
+	}
+}
+
+/* Generate new select field */
+func NewSelectField(name string, items []string, value string) *FormField {
+	return &FormField{
+		Name:   name,
+		Select: items,
+		Value:  value,
+		Kind:   "select",
 	}
 }
 
@@ -35,8 +65,7 @@ func NewCsrfField(w http.ResponseWriter, r *http.Request, secure bool) *FormFiel
 	return &FormField{
 		Name:   csrfFieldName,
 		Value:  csrfToken(w, r, secure),
-		Class:  "form-control",
-		Type:   "hidden",
+		Kind:   "hidden",
 		Sticky: true,
 		Validators: ValidatorsList{
 			ValidCSRF(r),
@@ -44,8 +73,11 @@ func NewCsrfField(w http.ResponseWriter, r *http.Request, secure bool) *FormFiel
 	}
 }
 
-/* Generate new CharField field with type password */
-func NewPasswordField(Name string) *FormField {
-	field := NewCharField(Name, nil).SetType("password")
-	return field
+/* Generate a new hidden text field */
+func NewHiddenField(name string, value *string) *FormField {
+	return &FormField{
+		Name:  name,
+		Value: strFromPtr(value),
+		Kind:  "hidden",
+	}
 }

+ 9 - 0
fallbackerror.go

@@ -0,0 +1,9 @@
+package forms
+
+/* fallbackError returns err if not nil or defaultErr otherwise */
+func fallbackError(err error, defaultErr error) error {
+	if err != nil {
+		return err
+	}
+	return defaultErr
+}

+ 47 - 0
fields.html

@@ -0,0 +1,47 @@
+{{define "checkbox"}}
+<div class="form-check">
+  <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}}
+{{if .Error}}{{range $e := .Error}}<div class="text-danger">{{$e}}</div>{{end}}{{end}}
+{{end}}
+
+{{define "password"}}
+{{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
+<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}}
+    {{- if .AutoFocus}} autofocus{{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}}
+{{end}}
+
+{{define "text"}}
+{{if .Label}}<label class="form-label" for="{{.Name}}">{{.Label}}</label>{{end}}
+<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}}

+ 55 - 43
forms.go

@@ -3,7 +3,7 @@ package forms
 import (
 	"bytes"
 	"context"
-	"embed"
+	_ "embed"
 	"html/template"
 	"strconv"
 )
@@ -16,18 +16,21 @@ type ValidatorsList []ValidatorFunc
 
 /* A general purpose form  field struct */
 type FormField struct {
-	Name        string
-	Error       []error
-	Value       string
-	Label       string
-	Class       string
-	Type        string
-	Placeholder string
-	Help        string
-	Required    bool
-	AutoFocus   bool
-	Sticky      bool
-	Validators  ValidatorsList
+	Name         string
+	Error        []error
+	Value        string
+	Label        string
+	Kind         string
+	Select       []string
+	Placeholder  string
+	Help         string
+	Required     bool
+	AutoFocus    bool
+	Sticky       bool
+	Checked      bool
+	AutoComplete string
+	Suffix       string
+	Validators   ValidatorsList
 }
 
 /* AddError appends a new error message to the form field errors list */
@@ -43,8 +46,10 @@ func (f *FormField) AddValidator(validator ValidatorFunc) *FormField {
 }
 
 /* SetValidators configures validators list in form field */
-func (f *FormField) SetValidators(validators ValidatorsList) *FormField {
-	f.Validators = validators
+func (f *FormField) SetValidators(validators ...ValidatorFunc) *FormField {
+	for _, validator := range validators {
+		f.AddValidator(validator)
+	}
 	return f
 }
 
@@ -54,12 +59,6 @@ func (f *FormField) SetLabel(label string) *FormField {
 	return f
 }
 
-/* SetClass configures class name in form field */
-func (f *FormField) SetClass(class string) *FormField {
-	f.Class = class
-	return f
-}
-
 /* SetRequired marks FormField as mandatory */
 func (f *FormField) SetRequired() *FormField {
 	f.Required = true
@@ -72,9 +71,9 @@ func (f *FormField) SetAutoFocus() *FormField {
 	return f
 }
 
-/* SetType specifies input field type */
-func (f *FormField) SetType(t string) *FormField {
-	f.Type = t
+/* SetKind specifies input field type */
+func (f *FormField) SetKind(t string) *FormField {
+	f.Kind = t
 	return f
 }
 
@@ -90,6 +89,21 @@ func (f *FormField) SetHelp(help string) *FormField {
 	return f
 }
 
+/* SetAutoComplete sets the autocomplete flag for a field */
+func (f *FormField) SetAutoComplete(on bool) *FormField {
+	f.AutoComplete = "off"
+	if on {
+		f.AutoComplete = "on"
+	}
+	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
@@ -104,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
 	}
@@ -117,28 +131,28 @@ 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
 	}
 	return 0.0
 }
 
-/* GetBool returns boolean value for checkbox fields */
+/* GetBool returns true if checkbox is selected, always returns nil error */
 func (f *FormField) GetBool() (bool, error) {
-	/* placeholder */
-	return false, nil
+	/* if box is checked, it sends a non-empty value */
+	return len(f.Value) != 0, nil
 }
 
-/* GetChecked returns true if checkbox has been checked and its value is "on" */
-func (f *FormField) GetChecked() bool {
-	return f.Value == "on"
+/* Bool returns true if checkbox is selected, ignores errors */
+func (f *FormField) Bool() bool {
+	return len(f.Value) != 0
 }
 
-/* formFieldTemplate is a template to render FormField element in HTML format */
-//go:embed templates/*
-var formTemplates embed.FS
+/* fieldsTemplateStr is a template to render FormField element in HTML format */
+//go:embed fields.html
+var fieldsTemplateStr string
 
 /* must checks for compile/start up errors */
 func must(r any, e error) any {
@@ -148,17 +162,15 @@ func must(r any, e error) any {
 	return r
 }
 
-/* formTemplate is compiled template to render FormField element in HTML format */
-var formTemplate = template.Must(
-	template.New("FormField").Parse(
-		string(must(formTemplates.ReadFile("templates/field.html")).([]byte)),
-	),
+/* fieldsTemplate is compiled template to render FormField element in HTML format */
+var fieldsTemplate = template.Must(
+	template.New("FormField").Parse(fieldsTemplateStr),
 )
 
 /* HTML renders FormField element in html format */
 func (f *FormField) HTML() template.HTML {
 	var buffer bytes.Buffer
-	if err := formTemplate.Execute(&buffer, f); err == nil {
+	if err := fieldsTemplate.ExecuteTemplate(&buffer, f.Kind, f); err == nil {
 		return template.HTML(buffer.String())
 	}
 	return template.HTML("")

+ 0 - 10
templates/field.html

@@ -1,10 +0,0 @@
-{{ if .Label }}<label class="form-label" for="{{ .Name }}">{{ .Label }}</label>{{ end }}
-<input type="{{ .Type }}" id="{{ .Name }}" name="{{ .Name }}"
-	{{- if .Class }} class="{{ .Class }}"{{ 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 }}
-	{{- if .AutoFocus }} autofocus{{ 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 }}

+ 2 - 2
validate.go

@@ -6,7 +6,7 @@ import (
 )
 
 /* ValidateForm parses a POST form into a pre-defined struct */
-func ValidateForm(r *http.Request, p any) (formErr error) {
+func ValidateForm(r *http.Request, p any, defaultErr error) (formErr error) {
 	/* asserts */
 	switch {
 	case r == nil:
@@ -56,7 +56,7 @@ func ValidateForm(r *http.Request, p any) (formErr error) {
 		}
 		/* assert on required fields */
 		if ff.Required && ff.Value == "" {
-			ff.Error = append(ff.Error, ERequiredField)
+			ff.Error = append(ff.Error, fallbackError(defaultErr, ERequiredField))
 			formErr = EFormHasErrors
 			continue
 		}

+ 177 - 83
validators.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net"
 	"net/http"
 	"net/mail"
 	"strings"
@@ -12,12 +13,9 @@ import (
 
 /* 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")
-	EInvalidEmail     = errors.New("invalid email address")
-	EInvalidDomain    = errors.New("invalid domain name")
-	EInvalidDomainTLD = errors.New("invalid domain TLD")
+	EInvalidInteger = errors.New("not a valid integer value")
+	EInvalidFloat   = errors.New("not a valid float value")
+	ERequired       = errors.New("this field is required")
 )
 
 /* A globaly defined set with valid domain TLDs */
@@ -65,33 +63,27 @@ func ValidLettersGeneric(Letters string, Error error) ValidatorFunc {
 	return Callback
 }
 
-/* ValidLength makes sure that a string length is between specified min and max values. */
-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))
+/* ValidLength makes sure that a string length is between specified vMin and vMax values. */
+func ValidLength(vMin, vMax int, err error) ValidatorFunc {
+	ELength := fmt.Errorf("must be a string between %d and %d characters in length", vMin, vMax)
 	return func(field *FormField, ctx context.Context) error {
-		if len(field.GetString()) != 0 && (len(field.GetString()) < min || len(field.GetString()) > max) {
-			return ELength
+		if len(field.GetString()) != 0 && (len(field.GetString()) < vMin || len(field.GetString()) > vMax) {
+			return fallbackError(err, ELength)
 		}
 		return nil
 	}
 }
 
 /* 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)
 	}
 }
 
@@ -105,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
 	}
@@ -146,79 +133,186 @@ 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 {
+func ValidFieldEqualTo(other *FormField, err error) ValidatorFunc {
+	defaultErr := errors.New("The text field must match other field's text exactly")
 	return func(field *FormField, ctx context.Context) error {
-		if field.GetString() != Other.GetString() {
-			return err
+		if field.GetString() != other.GetString() {
+			return fallbackError(err, defaultErr)
 		}
 		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
+/* ValidExct checks if text field has exact value */
+func ValidExact(text string, err error) ValidatorFunc {
+	defaultErr := fmt.Errorf("The the text field must be exactly '%s'", text)
+	return func(field *FormField, ctx context.Context) error {
+		if field.GetString() != text {
+			return fallbackError(err, defaultErr)
+		}
+		return nil
 	}
-	/* make sure domain name only contains allowed letters */
-	for _, r := range domain {
-		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
-			continue
+}
+
+/* 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 hostname[0] == '.' {
+			return fallbackError(err, EInvalidHostname)
+		}
+		/* 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, EInvalidHostname)
+		}
+		/* split to levels (tld, hostname[, subhostname...] */
+		levels := strings.Split(hostname, ".")
+		if len(levels) < minLevel {
+			return fallbackError(err, EInvalidHostname)
 		}
-		return EInvalidDomain
+		/* perform common checks on levels */
+		for _, level := range levels {
+			levelLen := len(level)
+			if levelLen == 0 {
+				continue
+			}
+			if levelLen > 63 {
+				return fallbackError(err, EInvalidHostname)
+			}
+			if level[0] == '-' || level[levelLen-1] == '-' {
+				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
 	}
-	/* split to levels (tld, domain[, subdomain...] */
-	levels := strings.Split(domain, ".")
-	levelsLen := len(levels)
-	if levelsLen < 2 {
-		return EInvalidDomain
+}
+
+/* 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 := validHostnameStr(2, err)
+	/* domain name checks */
+	return func(field *FormField, ctx context.Context) error {
+		return callbackCheck(field.GetString())
+	}
+}
+
+/* ValidDomainNameTLD checks if a field in a domain name has a whitelisted TLD */
+func ValidDomainNameTLD(err error) ValidatorFunc {
+	EInvalidDomainTLD := errors.New("invalid domain TLD")
+	return func(field *FormField, ctx context.Context) error {
+		/* make sure tld has proper size and is whitelisted */
+		domain := strings.ToLower(strings.TrimSpace(field.GetString()))
+		levels := strings.Split(domain, ".")
+		domainTLD := levels[len(levels)-1]
+		if len(domainTLD) < 2 {
+			return fallbackError(err, EInvalidDomainTLD)
+		}
+		if _, ok := validTLDs[domainTLD]; !ok {
+			return fallbackError(err, EInvalidDomainTLD)
+		}
+		return nil
 	}
-	/* perform common checks on levels */
-	for _, level := range levels {
-		levelLen := len(level)
-		if levelLen == 0 || levelLen > 63 {
-			return EInvalidDomain
+}
+
+/* ValidEmailLocal checks a valid local part for email address */
+func ValidEmailLocal(err error) ValidatorFunc {
+	EInvalidEmail := errors.New("invalid email address")
+	validLetters := ValidLettersGeneric(
+		"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+",
+		fallbackError(err, EInvalidEmail),
+	)
+	return func(field *FormField, ctx context.Context) error {
+		/* check for allowed symbols in local part */
+		if err := validLetters(field, ctx); err != nil {
+			return err
 		}
-		if level[0] == '-' || level[levelLen-1] == '-' {
-			return EInvalidDomain
+		/* common checks */
+		local := field.GetString()
+		if len(local) > 64 || local[0] == '.' || local[len(local)-1] == '.' || strings.Index(local, "..") != -1 {
+			return fallbackError(err, EInvalidEmail)
 		}
+		return nil
+	}
+}
 
+/* ValidEmail checks if field contains a valid email address */
+func ValidEmail(err error) ValidatorFunc {
+	EInvalidEmail := errors.New("invalid email address")
+	domainValidator := validHostnameStr(2, nil)
+	return func(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 fallbackError(err, EInvalidEmail)
+		}
+		/* use mail.ParseAddress on the sanitized text */
+		addr, err := mail.ParseAddress(addrStr)
+		if err != nil || addr.Address != addrStr {
+			return fallbackError(err, EInvalidEmail)
+		}
+		/* make sure email address has a valid domain name */
+		domain := strings.SplitN(addrStr, "@", 2)[1]
+		if err := domainValidator(domain); err != nil {
+			return fallbackError(err, EInvalidEmail)
+		}
+		return nil
 	}
-	/* make sure tld has proper size and is whitelisted */
-	domainTLD := levels[len(levels)-1]
-	if len(domainTLD) < 2 {
-		return EInvalidDomainTLD
+}
+
+/* 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
 	}
-	if _, ok := validTLDs[domainTLD]; !ok {
-		return EInvalidDomainTLD
+}
+
+/* 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
 	}
-	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
+/* 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
 	}
-	return nil
 }
 
 /* ValidCSRF checks if CSRF token is valid */