Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
473 lines
13 KiB
Go
473 lines
13 KiB
Go
package values
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"net/mail"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/locale"
|
|
|
|
"github.com/cortezaproject/corteza/server/compose/types"
|
|
"github.com/cortezaproject/corteza/server/pkg/expr"
|
|
"github.com/cortezaproject/corteza/server/pkg/slice"
|
|
"github.com/cortezaproject/corteza/server/store"
|
|
)
|
|
|
|
// Validator package provides tooling to validate
|
|
// record and it's values against field constraints
|
|
//
|
|
// Structures and basic logic is similar to what we offer on the frontend
|
|
// (see corteza-js/validator package) but with less features as there
|
|
// is no need for such level of interaction and dynamic we require on the frontend
|
|
|
|
type (
|
|
UniqueChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (uint64, error)
|
|
ReferenceChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (bool, error)
|
|
|
|
localeService interface {
|
|
T(ctx context.Context, ns, key string, rr ...string) string
|
|
TResource(ctx context.Context, ns, key string, rr ...string) string
|
|
}
|
|
|
|
validator struct {
|
|
uniqueCheckerFn UniqueChecker
|
|
recordRefCheckerFn ReferenceChecker
|
|
userRefCheckerFn ReferenceChecker
|
|
fileRefCheckerFn ReferenceChecker
|
|
|
|
localeSvc localeService
|
|
|
|
now func() time.Time
|
|
}
|
|
)
|
|
|
|
func makeInternalErr(field *types.ModuleField, err error) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "internal",
|
|
Message: err.Error(),
|
|
Meta: map[string]interface{}{"field": field.Name},
|
|
}
|
|
}
|
|
|
|
func makeEmptyErr(ctx context.Context, field *types.ModuleField, ls localeService) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "empty",
|
|
Meta: map[string]interface{}{"field": field.Name},
|
|
Message: ls.T(ctx, "compose", "record-field.errors.empty"),
|
|
}
|
|
}
|
|
|
|
func makeInvalidValueErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "invalidValue",
|
|
Meta: map[string]interface{}{"field": field.Name, "value": value},
|
|
Message: ls.T(ctx, "compose", "record-field.errors.invalidValue"),
|
|
}
|
|
}
|
|
|
|
func makeInvalidRefErr(ctx context.Context, field *types.ModuleField, ref uint64, ls localeService) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "invalidRef",
|
|
Meta: map[string]interface{}{"field": field.Name, "ref": ref},
|
|
Message: ls.T(ctx, "compose", "record-field.errors.invalidRef"),
|
|
}
|
|
}
|
|
|
|
func makeDuplicateValueInSetErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "duplicateValueInSet",
|
|
Meta: map[string]interface{}{"field": field.Name, "value": value},
|
|
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValueInSet"),
|
|
}
|
|
}
|
|
|
|
func makeDuplicateValueErr(ctx context.Context, field *types.ModuleField, recordID uint64, ls localeService) types.RecordValueError {
|
|
return types.RecordValueError{
|
|
Kind: "duplicateValue",
|
|
Meta: map[string]interface{}{"field": field.Name, "recordID": recordID},
|
|
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValue"),
|
|
}
|
|
}
|
|
|
|
// Simple wrapper for easier error returning from validation functions
|
|
func e2s(ee ...types.RecordValueError) []types.RecordValueError {
|
|
return ee
|
|
}
|
|
|
|
func Validator() *validator {
|
|
return &validator{
|
|
now: func() time.Time { return time.Now() },
|
|
localeSvc: locale.Global(),
|
|
}
|
|
}
|
|
|
|
func (vldtr *validator) UniqueChecker(fn UniqueChecker) {
|
|
vldtr.uniqueCheckerFn = fn
|
|
}
|
|
|
|
func (vldtr *validator) RecordRefChecker(fn ReferenceChecker) {
|
|
vldtr.recordRefCheckerFn = fn
|
|
}
|
|
|
|
func (vldtr *validator) UserRefChecker(fn ReferenceChecker) {
|
|
vldtr.userRefCheckerFn = fn
|
|
}
|
|
|
|
func (vldtr *validator) FileRefChecker(fn ReferenceChecker) {
|
|
vldtr.fileRefCheckerFn = fn
|
|
}
|
|
|
|
// Run validates record and it's values against module & module fields options
|
|
//
|
|
// Validation is done in phases for optimal resource usage:
|
|
// - check if required values are present
|
|
// - check for unique-multi-value in multi value fields
|
|
// - field-kind specific validation on all values
|
|
// - unique check on all values
|
|
func (vldtr validator) Run(ctx context.Context, s store.Storer, m *types.Module, r *types.Record) (out *types.RecordValueErrorSet) {
|
|
var (
|
|
f *types.ModuleField
|
|
|
|
valParser = expr.Parser()
|
|
valDict = r.Values.Dict(m.Fields)
|
|
)
|
|
|
|
out = &types.RecordValueErrorSet{}
|
|
|
|
fields:
|
|
for _, f := range m.Fields {
|
|
vv := r.Values.FilterByName(f.Name)
|
|
|
|
if f.Required {
|
|
if len(vv) == 0 {
|
|
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
|
|
continue fields
|
|
}
|
|
|
|
for _, v := range vv {
|
|
if len(v.Value) == 0 || (f.IsRef() && v.Ref == 0) {
|
|
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
|
|
continue fields
|
|
}
|
|
}
|
|
}
|
|
|
|
if f.Multi && f.Options.IsUniqueMultiValue() {
|
|
flipped := make(map[string]bool)
|
|
for _, v := range vv {
|
|
if flipped[v.Value] {
|
|
out.Push(makeDuplicateValueInSetErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
continue fields
|
|
}
|
|
|
|
flipped[v.Value] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, v := range r.Values {
|
|
if !v.IsUpdated() || v.IsDeleted() {
|
|
// We'll validate only updated (and non-deleted) values
|
|
continue
|
|
}
|
|
|
|
if f = m.Fields.FindByName(v.Name); f == nil {
|
|
continue
|
|
}
|
|
|
|
if f.Expressions.ValueExpr != "" {
|
|
// do not do any validation if field has value expression!
|
|
continue
|
|
}
|
|
|
|
if !(f.Expressions.DisableDefaultValidators && len(f.Expressions.Validators) > 0) {
|
|
if v.Value == "" {
|
|
// Nothing to do with empty value
|
|
return nil
|
|
}
|
|
|
|
// Per field type validators
|
|
switch strings.ToLower(f.Kind) {
|
|
case "bool":
|
|
out.Push(vldtr.vBool(ctx, v, f, r, m)...)
|
|
case "datetime":
|
|
out.Push(vldtr.vDatetime(ctx, v, f, r, m)...)
|
|
case "email":
|
|
out.Push(vldtr.vEmail(ctx, v, f, r, m)...)
|
|
case "file":
|
|
out.Push(vldtr.vFile(ctx, s, v, f, r, m)...)
|
|
case "number":
|
|
out.Push(vldtr.vNumber(ctx, v, f, r, m)...)
|
|
case "record":
|
|
out.Push(vldtr.vRecord(ctx, s, v, f, r, m)...)
|
|
case "select":
|
|
out.Push(vldtr.vSelect(ctx, v, f, r, m)...)
|
|
//case "string":
|
|
// out.Push(vldtr.vString(v, f, r, m)...)
|
|
case "url":
|
|
out.Push(vldtr.vUrl(ctx, v, f, r, m)...)
|
|
case "user":
|
|
out.Push(vldtr.vUser(ctx, s, v, f, r, m)...)
|
|
}
|
|
}
|
|
|
|
if len(f.Expressions.Validators) > 0 {
|
|
for i, cv := range f.Expressions.Validators {
|
|
eval, err := valParser.NewEvaluable(cv.Test)
|
|
if err != nil {
|
|
out.Push(makeInternalErr(f, err))
|
|
break
|
|
}
|
|
|
|
invalid, err := eval.EvalBool(ctx, map[string]interface{}{
|
|
"value": v.Value,
|
|
"oldValue": v.OldValue,
|
|
"values": valDict,
|
|
})
|
|
|
|
if err != nil {
|
|
out.Push(makeInternalErr(f, err))
|
|
break
|
|
}
|
|
|
|
if invalid {
|
|
message := cv.Error
|
|
|
|
// In case tests mit this for easier testing; falling back to whatever
|
|
// the field validator has stored.
|
|
if vldtr.localeSvc != nil {
|
|
validatorID := locale.ContentID(cv.ValidatorID, i)
|
|
rpl := strings.NewReplacer(
|
|
"{{validatorID}}", strconv.FormatUint(validatorID, 10),
|
|
)
|
|
|
|
k := rpl.Replace(types.LocaleKeyModuleFieldExpressionValidatorValidatorIDError.Path)
|
|
message = vldtr.localeSvc.TResource(ctx, f.ResourceTranslation(), k)
|
|
}
|
|
|
|
out.Push(types.RecordValueError{
|
|
Kind: "error",
|
|
Message: message,
|
|
Meta: map[string]interface{}{"field": f.Name}},
|
|
)
|
|
|
|
// break at first failed test
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is the most resource-heavy operation
|
|
// we'll do in at the end
|
|
for _, v := range r.Values {
|
|
if f = m.Fields.FindByName(v.Name); f == nil {
|
|
continue
|
|
}
|
|
|
|
if !f.Options.IsUnique() {
|
|
// Only interested in unique fields
|
|
continue
|
|
}
|
|
|
|
if vldtr.uniqueCheckerFn == nil {
|
|
return nil
|
|
}
|
|
duplicateRecordID, err := vldtr.uniqueCheckerFn(ctx, s, v, f, m)
|
|
if err != nil {
|
|
out.Push(makeInternalErr(f, err))
|
|
} else if duplicateRecordID > 0 && duplicateRecordID != r.ID {
|
|
out.Push(makeDuplicateValueErr(ctx, f, duplicateRecordID, vldtr.localeSvc))
|
|
}
|
|
}
|
|
|
|
if out.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func (vldtr validator) vBool(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if v.Value == "" {
|
|
return nil
|
|
}
|
|
|
|
if v.Value != strBoolTrue && v.Value != strBoolFalse && v.Value != strBoolFalseAlt {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vDatetime(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
var (
|
|
inputFormat string
|
|
t time.Time
|
|
err error
|
|
|
|
// We'll validate against this value
|
|
refTime = vldtr.now()
|
|
)
|
|
|
|
if f.Options.Bool(fieldOpt_Datetime_onlyDate) {
|
|
inputFormat = datetimeInternalFormatDate
|
|
|
|
// Round down ref time to midnight
|
|
refTime = time.Date(refTime.Year(), refTime.Month(), refTime.Day(), 0, 0, 0, 0, refTime.Location())
|
|
} else if f.Options.Bool(fieldOpt_Datetime_onlyTime) {
|
|
inputFormat = datetimeIntenralFormatTime
|
|
|
|
// Round down ref time to day one
|
|
refTime = time.Date(0, 1, 1, refTime.Hour(), refTime.Minute(), refTime.Second(), refTime.Nanosecond(), refTime.Location())
|
|
} else {
|
|
inputFormat = datetimeInternalFormatFull
|
|
}
|
|
|
|
t, err = time.Parse(inputFormat, v.Value)
|
|
if err != nil {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
if f.Options.Bool(fieldOpt_Datetime_onlyFutureValues) {
|
|
if !t.After(refTime) {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
} else if f.Options.Bool(fieldOpt_Datetime_onlyPastValues) {
|
|
if !t.Before(refTime) {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
}
|
|
|
|
// @todo check past/future
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vEmail(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if _, err := mail.ParseAddress(v.Value); err != nil {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vFile(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if vldtr.fileRefCheckerFn == nil {
|
|
return nil
|
|
}
|
|
if ok, err := vldtr.fileRefCheckerFn(ctx, s, v, f, m); err != nil {
|
|
return e2s(makeInternalErr(f, err))
|
|
} else if !ok {
|
|
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vNumber(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
// @todo !! This is temporary; precision and scale require a rework
|
|
if _, _, err := big.ParseFloat(v.Value, 0, maxPrecision, big.ToNearestEven); err != nil {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vRecord(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if vldtr.recordRefCheckerFn == nil {
|
|
return nil
|
|
}
|
|
|
|
if ok, err := vldtr.recordRefCheckerFn(ctx, s, v, f, m); err != nil {
|
|
return e2s(makeInternalErr(f, err))
|
|
} else if !ok {
|
|
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vSelect(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
var (
|
|
sbm = make(map[string]bool)
|
|
options, has = f.Options["options"]
|
|
)
|
|
|
|
if !has {
|
|
return nil
|
|
}
|
|
|
|
switch oo := options.(type) {
|
|
case []string:
|
|
sbm = slice.ToStringBoolMap(oo)
|
|
case []interface{}:
|
|
for _, o := range oo {
|
|
switch c := o.(type) {
|
|
case string:
|
|
sbm[c] = true
|
|
case map[string]string:
|
|
if value, has := c["value"]; has {
|
|
sbm[value] = true
|
|
}
|
|
case map[string]interface{}:
|
|
if value, has := c["value"]; has {
|
|
if value, ok := value.(string); ok {
|
|
sbm[value] = true
|
|
}
|
|
}
|
|
case types.ModuleFieldOptions:
|
|
if value, has := c["value"]; has {
|
|
if value, ok := value.(string); ok {
|
|
sbm[value] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(sbm) == 0 {
|
|
return e2s(makeInternalErr(f, fmt.Errorf("invalid select options definition")))
|
|
}
|
|
|
|
if !sbm[v.Value] {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//func (vldtr validator) vString(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
// return nil
|
|
//}
|
|
|
|
func (vldtr validator) vUrl(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if p, err := url.Parse(v.Value); err != nil {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
} else if p.Scheme == "" || p.Host == "" {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
} else if f.Options.Bool(fieldOpt_Url_onlySecure) && p.Scheme != "https" {
|
|
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vldtr validator) vUser(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
|
|
if vldtr.userRefCheckerFn == nil {
|
|
return nil
|
|
}
|
|
if ok, err := vldtr.userRefCheckerFn(ctx, s, v, f, m); err != nil {
|
|
return e2s(makeInternalErr(f, err))
|
|
} else if !ok {
|
|
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
|
|
}
|
|
|
|
return nil
|
|
}
|