529 lines
12 KiB
Go
529 lines
12 KiB
Go
package pkg
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/jackc/pgtype"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var tagsRegex = regexp.MustCompile("[^a-z0-9-]+")
|
|
|
|
type Attribute struct {
|
|
Key, Val string
|
|
}
|
|
|
|
type InputField struct {
|
|
Name string
|
|
Label string
|
|
Type string
|
|
Val string
|
|
Is string
|
|
Required bool
|
|
Attributes []template.HTMLAttr
|
|
Errors []error
|
|
}
|
|
|
|
func (field *InputField) Scan(value interface{}) error {
|
|
if value == nil {
|
|
field.Val = ""
|
|
return nil
|
|
}
|
|
switch v := value.(type) {
|
|
case time.Time:
|
|
if field.Type == "date" {
|
|
field.Val = v.Format("2006-01-02")
|
|
} else if field.Type == "time" {
|
|
field.Val = v.Format("15:04")
|
|
} else {
|
|
field.Val = v.Format(time.RFC3339)
|
|
}
|
|
default:
|
|
field.Val = fmt.Sprintf("%v", v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (field *InputField) Value() (driver.Value, error) {
|
|
return field.Val, nil
|
|
}
|
|
|
|
func (field *InputField) FillValue(r *http.Request) {
|
|
field.Val = strings.TrimSpace(r.FormValue(field.Name))
|
|
}
|
|
|
|
func (field *InputField) Integer() int {
|
|
value, err := strconv.Atoi(field.Val)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (field *InputField) IntegerOrNil() interface{} {
|
|
if field.Val != "" {
|
|
if i := field.Integer(); i > 0 {
|
|
return i
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (field *InputField) Float64() float64 {
|
|
value, err := strconv.ParseFloat(field.Val, 64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (field *InputField) String() string {
|
|
return field.Val
|
|
}
|
|
|
|
type SelectOption struct {
|
|
Value string
|
|
Label string
|
|
Group string
|
|
}
|
|
|
|
type SelectField struct {
|
|
Name string
|
|
Label string
|
|
Selected []string
|
|
Options []*SelectOption
|
|
Attributes []template.HTMLAttr
|
|
Required bool
|
|
Multiple bool
|
|
EmptyLabel string
|
|
Errors []error
|
|
}
|
|
|
|
func (field *SelectField) Scan(value interface{}) error {
|
|
if value == nil {
|
|
field.Selected = append(field.Selected, "")
|
|
return nil
|
|
}
|
|
if str, ok := value.(string); ok {
|
|
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
|
|
for _, element := range array.Elements {
|
|
field.Selected = append(field.Selected, element)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
field.Selected = append(field.Selected, fmt.Sprintf("%v", value))
|
|
return nil
|
|
}
|
|
|
|
func (field *SelectField) Value() (driver.Value, error) {
|
|
return field.String(), nil
|
|
}
|
|
|
|
func (field *SelectField) String() string {
|
|
if field.Selected == nil {
|
|
return ""
|
|
}
|
|
return field.Selected[0]
|
|
}
|
|
|
|
func (field *SelectField) OrNull() interface{} {
|
|
if field.String() == "" {
|
|
return sql.NullString{}
|
|
}
|
|
return field
|
|
}
|
|
|
|
func (field *SelectField) FillValue(r *http.Request) {
|
|
field.Selected = r.Form[field.Name]
|
|
}
|
|
|
|
func (field *SelectField) HasValidOptions() bool {
|
|
for _, selected := range field.Selected {
|
|
if !field.isValidOption(selected) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (field *SelectField) IsSelected(v string) bool {
|
|
for _, selected := range field.Selected {
|
|
if selected == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (field *SelectField) FindOption(value string) *SelectOption {
|
|
for _, option := range field.Options {
|
|
if option.Value == value {
|
|
return option
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (field *SelectField) isValidOption(selected string) bool {
|
|
return field.FindOption(selected) != nil
|
|
}
|
|
|
|
func (field *SelectField) Clear() {
|
|
field.Selected = []string{}
|
|
}
|
|
|
|
func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
|
|
rows, err := conn.Query(ctx, sql, args...)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var options []*SelectOption
|
|
for rows.Next() {
|
|
option := &SelectOption{}
|
|
err = rows.Scan(&option.Value, &option.Label)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
options = append(options, option)
|
|
}
|
|
if rows.Err() != nil {
|
|
panic(rows.Err())
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
func MustGetGroupedOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
|
|
rows, err := conn.Query(ctx, sql, args...)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var options []*SelectOption
|
|
for rows.Next() {
|
|
option := &SelectOption{}
|
|
err = rows.Scan(&option.Value, &option.Label, &option.Group)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
options = append(options, option)
|
|
}
|
|
if rows.Err() != nil {
|
|
panic(rows.Err())
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption {
|
|
return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language)
|
|
}
|
|
|
|
type RadioOption struct {
|
|
Value string
|
|
Label string
|
|
}
|
|
|
|
type RadioField struct {
|
|
Name string
|
|
Label string
|
|
Selected string
|
|
Options []*RadioOption
|
|
Attributes []template.HTMLAttr
|
|
Required bool
|
|
Errors []error
|
|
}
|
|
|
|
func (field *RadioField) Scan(value interface{}) error {
|
|
if value == nil {
|
|
field.Selected = ""
|
|
return nil
|
|
}
|
|
field.Selected = fmt.Sprintf("%v", value)
|
|
return nil
|
|
}
|
|
|
|
func (field *RadioField) Value() (driver.Value, error) {
|
|
return field.Selected, nil
|
|
}
|
|
|
|
func (field *RadioField) String() string {
|
|
return field.Selected
|
|
}
|
|
|
|
func (field *RadioField) FillValue(r *http.Request) {
|
|
field.Selected = strings.TrimSpace(r.FormValue(field.Name))
|
|
}
|
|
|
|
func (field *RadioField) IsSelected(v string) bool {
|
|
return field.Selected == v
|
|
}
|
|
|
|
func (field *RadioField) FindOption(value string) *RadioOption {
|
|
for _, option := range field.Options {
|
|
if option.Value == value {
|
|
return option
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (field *RadioField) isValidOption(selected string) bool {
|
|
return field.FindOption(selected) != nil
|
|
}
|
|
|
|
type CheckField struct {
|
|
Name string
|
|
Label string
|
|
Checked bool
|
|
Attributes []template.HTMLAttr
|
|
Required bool
|
|
Errors []error
|
|
}
|
|
|
|
func (field *CheckField) FillValue(r *http.Request) {
|
|
field.Checked = len(r.Form[field.Name]) > 0
|
|
}
|
|
|
|
func (field *CheckField) Scan(value interface{}) error {
|
|
if value == nil {
|
|
field.Checked = false
|
|
return nil
|
|
}
|
|
switch v := value.(type) {
|
|
case bool:
|
|
field.Checked = v
|
|
default:
|
|
field.Checked, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (field *CheckField) Value() (driver.Value, error) {
|
|
return field.Checked, nil
|
|
}
|
|
|
|
type FileField struct {
|
|
Name string
|
|
Label string
|
|
MaxSize int64
|
|
OriginalFileName string
|
|
ContentType string
|
|
Content []byte
|
|
Errors []error
|
|
}
|
|
|
|
func (field *FileField) FillValue(r *http.Request) error {
|
|
file, header, err := r.FormFile(field.Name)
|
|
if err != nil {
|
|
if err == http.ErrMissingFile {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
field.Content, err = ioutil.ReadAll(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
field.OriginalFileName = header.Filename
|
|
field.ContentType = header.Header.Get("Content-Type")
|
|
if len(field.Content) == 0 {
|
|
field.ContentType = http.DetectContentType(field.Content)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type TagsField struct {
|
|
Name string
|
|
Label string
|
|
Tags []string
|
|
Attributes []template.HTMLAttr
|
|
Required bool
|
|
Errors []error
|
|
}
|
|
|
|
func (field *TagsField) Value() (driver.Value, error) {
|
|
if field.Tags == nil {
|
|
return []string{}, nil
|
|
}
|
|
return field.Tags, nil
|
|
}
|
|
|
|
func (field *TagsField) Scan(value interface{}) error {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if str, ok := value.(string); ok {
|
|
if array, err := pgtype.ParseUntypedTextArray(str); err == nil {
|
|
for _, element := range array.Elements {
|
|
field.Tags = append(field.Tags, element)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
field.Tags = append(field.Tags, fmt.Sprintf("%v", value))
|
|
return nil
|
|
}
|
|
|
|
func (field *TagsField) FillValue(r *http.Request) {
|
|
field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",")
|
|
if len(field.Tags) == 1 && len(field.Tags[0]) == 0 {
|
|
field.Tags = []string{}
|
|
}
|
|
}
|
|
|
|
func (field *TagsField) String() string {
|
|
return strings.Join(field.Tags, ",")
|
|
}
|
|
|
|
type ToggleField struct {
|
|
Name string
|
|
Label string
|
|
Selected string
|
|
FirstOption *ToggleOption
|
|
SecondOption *ToggleOption
|
|
Attributes []template.HTMLAttr
|
|
Errors []error
|
|
}
|
|
|
|
type ToggleOption struct {
|
|
Value string
|
|
Label string
|
|
Description string
|
|
}
|
|
|
|
func (field *ToggleField) FillValue(r *http.Request) {
|
|
field.Selected = strings.TrimSpace(r.FormValue(field.Name))
|
|
if field.Selected != field.FirstOption.Value && field.Selected != field.SecondOption.Value {
|
|
field.Selected = field.FirstOption.Value
|
|
}
|
|
}
|
|
|
|
type FormValidator struct {
|
|
Valid bool
|
|
}
|
|
|
|
func newFormValidator() *FormValidator {
|
|
return &FormValidator{true}
|
|
}
|
|
|
|
func (v *FormValidator) AllOK() bool {
|
|
return v.Valid
|
|
}
|
|
|
|
func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool {
|
|
return v.checkInput(field, field.Val != "", message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message string) bool {
|
|
return v.checkInput(field, len(field.Val) >= min, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool {
|
|
_, err := mail.ParseAddress(field.Val)
|
|
return v.checkInput(field, err == nil, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidVATINInput(field *InputField, country string, message string) bool {
|
|
// TODO: actual VATIN validation
|
|
return v.checkInput(field, true, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, message string) bool {
|
|
// TODO: actual phone validation
|
|
return v.checkInput(field, true, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidIBANInput(field *InputField, message string) bool {
|
|
// TODO: actual IBAN validation
|
|
return v.checkInput(field, true, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidBICInput(field *InputField, message string) bool {
|
|
// TODO: actual BIC validation
|
|
return v.checkInput(field, true, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
|
|
return v.checkInput(confirm, password.Val == confirm.Val, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool {
|
|
return v.checkSelect(field, field.HasValidOptions(), message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool {
|
|
repeated := false
|
|
groups := map[string]bool{}
|
|
for _, selected := range field.Selected {
|
|
option := field.FindOption(selected)
|
|
if exists := groups[option.Group]; exists {
|
|
repeated = true
|
|
break
|
|
}
|
|
groups[option.Group] = true
|
|
}
|
|
return v.checkSelect(field, !repeated, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidURL(field *InputField, message string) bool {
|
|
_, err := url.ParseRequestURI(field.Val)
|
|
return v.checkInput(field, err == nil, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidDate(field *InputField, message string) bool {
|
|
_, err := time.Parse("2006-01-02", field.Val)
|
|
return v.checkInput(field, err == nil, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool {
|
|
pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$"
|
|
match, err := regexp.MatchString(pattern, field.Val)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return v.checkInput(field, match, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, message string) bool {
|
|
value, err := strconv.Atoi(field.Val)
|
|
return v.checkInput(field, err == nil && value >= min && value <= max, message)
|
|
}
|
|
|
|
func (v *FormValidator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool {
|
|
value, err := strconv.ParseFloat(field.Val, 64)
|
|
return v.checkInput(field, err == nil && value >= min && value <= max, message)
|
|
}
|
|
|
|
func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool {
|
|
if !ok {
|
|
field.Errors = append(field.Errors, errors.New(message))
|
|
v.Valid = false
|
|
}
|
|
return ok
|
|
}
|
|
|
|
func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) bool {
|
|
if !ok {
|
|
field.Errors = append(field.Errors, errors.New(message))
|
|
v.Valid = false
|
|
}
|
|
return ok
|
|
}
|